Kontroler w MVC - Na Przykładzie Kontrolera Rejestracji
Czym jest kontroler w architekturze MVC?
Kontroler (Controller) to środkowa warstwa w architekturze MVC (Model-View-Controller), która pełni rolę koordynatora między warstwą prezentacji (View) a warstwą danych (Model). Odpowiada za odbieranie żądań HTTP, przetwarzanie logiki biznesowej i zwracanie odpowiedzi użytkownikowi.
Kluczowe odpowiedzialności kontrolera
- Odbieranie żądań - przechwytywanie żądań HTTP z routera
- Walidacja danych - weryfikacja poprawności danych wejściowych
- Koordynacja modeli - wywoływanie odpowiednich operacji na modelach
- Zwracanie odpowiedzi - generowanie odpowiedzi HTTP (widoki, JSON, przekierowania)
Kontroler rejestracji - kompletny przykład
Kontroler rejestracji to idealny przykład pokazujący wszystkie aspekty pracy kontrolera w nowoczesnym frameworku PHP. Obsługuje on dwa endpointy - wyświetlanie formularza rejestracji (GET) oraz przetwarzanie rejestracji (POST).
Kod kontrolera RegisterController
<?php
namespace App\Controllers;
use App\FormValidators\RegisterFormDTO;
use App\Mail\WelcomeMailable;
use DJWeb\Framework\Auth\Auth;
use DJWeb\Framework\DBAL\Models\Entities\Role;
use DJWeb\Framework\DBAL\Models\Entities\User;
use DJWeb\Framework\Http\Response;
use DJWeb\Framework\Mail\MailerFactory;
use DJWeb\Framework\Routing\Attributes\Route;
use DJWeb\Framework\Routing\Attributes\RouteGroup;
use DJWeb\Framework\Routing\Controller;
use DJWeb\Framework\View\Inertia\Inertia;
use Psr\Http\Message\ResponseInterface;
#[RouteGroup('auth')]
class RegisterController extends Controller
{
#[Route('/register', methods: 'GET')]
public function register(): ResponseInterface
{
return Inertia::render('Auth/Register.vue', ['title' => 'Register']);
}
#[Route('/register', methods: 'POST')]
public function store(RegisterFormDTO $request): ResponseInterface
{
// 1. Tworzenie nowego użytkownika
$user = new User()->fill($request->toArray());
$user->save();
// 2. Przypisanie domyślnej roli
$defaultRole = Role::query()->select()->where('name', '=', 'user')->first();
if($defaultRole) {
$user->addRole($defaultRole);
}
// 3. Wysłanie maila powitalnego
MailerFactory::createSmtpMailer(...Config::get('mail.default'))
->send(new WelcomeMailable($user));
// 4. Automatyczne zalogowanie użytkownika
Auth::login($user);
// 5. Przekierowanie na stronę główną
return new Response()
->withHeader('Location', '/')
->withStatus(303);
}
}
Anatomia kontrolera - krok po kroku
1. Definiowanie tras za pomocą atrybutów
Nowoczesne frameworki PHP wykorzystują atrybuty (dostępne od PHP 8.0) do definiowania tras bezpośrednio w kontrolerach. To podejście zapewnia:
- Przejrzystość - trasy są zdefiniowane obok metod, które obsługują
- Automatyczną rejestrację - framework automatycznie skanuje kontrolery
- Single Responsibility - każdy kontroler "wie" tylko o swoich trasach
Atrybut RouteGroup
#[RouteGroup('auth')]
class RegisterController extends Controller
Atrybut RouteGroup grupuje wszystkie trasy kontrolera pod wspólnym prefiksem. W tym przypadku trasy będą dostępne pod /auth/register zamiast samego /register.
Atrybut Route
#[Route('/register', methods: 'GET')]
public function register(): ResponseInterface
Atrybut Route definiuje konkretną trasę dla metody kontrolera, określając ścieżkę i metody HTTP (GET, POST, PUT, DELETE, PATCH).
2. Wyświetlanie formularza - metoda register()
public function register(): ResponseInterface
{
return Inertia::render('Auth/Register.vue', ['title' => 'Register']);
}
Pierwsza metoda kontrolera jest prosta - renderuje komponent Vue.js przy użyciu Inertia.js. Przekazuje do widoku dane (tutaj tylko tytuł strony). Ta metoda obsługuje żądania GET na /auth/register.
3. Przetwarzanie rejestracji - metoda store()
Metoda store() to serce kontrolera rejestracji. Wykonuje pięć kluczowych kroków:
Krok 1: Tworzenie użytkownika
$user = new User()->fill($request->toArray());
$user->save();
Wykorzystuje wzorzec Active Record - model User jest wypełniany danymi z zwalidowanego żądania i zapisywany w bazie danych. Metoda fill() automatycznie mapuje pola z DTO na pola modelu.
Krok 2: Przypisanie domyślnej roli
$defaultRole = Role::query()->select()->where('name', '=', 'user')->first();
if($defaultRole) {
$user->addRole($defaultRole);
}
System autoryzacji oparty na rolach (RBAC - Role-Based Access Control). Każdy nowy użytkownik domyślnie otrzymuje rolę "user". Metoda addRole() wykorzystuje QueryBuilder do dodania wpisu w tabeli pośredniej user_roles.
Krok 3: Wysłanie maila powitalnego
MailerFactory::createSmtpMailer(...Config::get('mail.default'))
->send(new WelcomeMailable($user));
Framework posiada własną implementację systemu wysyłki maili opartego na Symfony/Mailer. Używa wzorca Factory do utworzenia odpowiedniego mailera (SMTP, Sendmail) i wysyła mail powitalny do nowego użytkownika.
Krok 4: Automatyczne zalogowanie
Auth::login($user);
Fasada Auth zarządza sesją użytkownika. Po rejestracji użytkownik jest automatycznie logowany, co poprawia UX - nie musi logować się ręcznie zaraz po rejestracji.
Krok 5: Przekierowanie
return new Response()
->withHeader('Location', '/')
->withStatus(303);
Zgodnie z PSR-7, odpowiedzi HTTP są immutable. Metoda withHeader() zwraca nową instancję odpowiedzi z ustawionym nagłówkiem przekierowania. Kod 303 (See Other) informuje przeglądarkę o przekierowaniu po operacji POST.
Automatyczna walidacja przez DTO
W metodzie store() zamiast tradycyjnego ServerRequestInterface przyjmujemy RegisterFormDTO. To kluczowy element systemu walidacji.
RegisterFormDTO - Data Transfer Object
<?php
namespace App\FormValidators;
use DJWeb\Framework\Validation\Attributes\Email;
use DJWeb\Framework\Validation\Attributes\IsValidated;
use DJWeb\Framework\Validation\Attributes\MinLength;
use DJWeb\Framework\Validation\Attributes\Required;
use DJWeb\Framework\Validation\Attributes\SameAs;
use DJWeb\Framework\Validation\FormRequest;
class RegisterFormDTO extends FormRequest
{
#[IsValidated]
#[Required(message: 'Username is required')]
#[MinLength(3, message: 'Username must be at least 3 characters')]
public protected(set) string $username;
#[IsValidated]
#[Required(message: 'Email is required')]
#[Email(message: 'Invalid email format')]
public protected(set) string $email;
#[IsValidated]
#[Required(message: 'Password is required')]
#[MinLength(8, message: 'Password must be at least 8 characters')]
public protected(set) string $password;
#[IsValidated]
#[Required(message: 'Password confirmation is required')]
#[SameAs('password', message: 'Passwords must match')]
public protected(set) string $password_confirmation;
}
Jak działa automatyczna walidacja?
Framework automatycznie wykrywa, że metoda kontrolera przyjmuje DTO dziedziczący po FormRequest:
- Tworzy instancję DTO
- Odczytuje atrybuty walidacyjne (
#[Required],#[Email], etc.) - Wykonuje walidację danych z żądania HTTP
- W przypadku błędów zwraca odpowiedź JSON z komunikatami
- W przypadku sukcesu przekazuje wypełnione DTO do kontrolera
Automatyczne wstrzykiwanie modeli do tras
Framework wspiera zaawansowane automatyczne bindowanie modeli do parametrów tras. Zobaczmy to na przykładzie kontrolera kategorii:
Przykład: CategoriesController
<?php
namespace App\Controllers;
use App\Database\Models\Category as CategoryModel;
use DJWeb\Framework\Routing\Attributes\Route;
use DJWeb\Framework\Routing\Attributes\RouteGroup;
use DJWeb\Framework\Routing\Attributes\RouteParam;
use DJWeb\Framework\Routing\Controller;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
#[RouteGroup('categories')]
class CategoriesController extends Controller
{
#[Route('/', methods: ['GET'])]
public function index(ServerRequestInterface $request): ResponseInterface
{
$categories = CategoryModel::query()->get();
return new Response()->withContent(json_encode($categories));
}
#[Route('/<category:\d+>', methods: ['GET'])]
#[RouteParam('category', bind: CategoryModel::class)]
public function show(
ServerRequestInterface $request,
CategoryModel $category
): ResponseInterface
{
return new Response()->withContent(json_encode($category));
}
}
Anatomia automatycznego bindowania
1. Parametr w trasie
#[Route('/<category:\d+>', methods: ['GET'])]
Składnia <category:\d+> definiuje parametr trasy:
category- nazwa parametru\d+- wyrażenie regularne (jedna lub więcej cyfr)
2. Atrybut RouteParam
#[RouteParam('category', bind: CategoryModel::class)]
Informuje framework, że parametr category powinien być automatycznie związany z modelem CategoryModel.
3. Type-hinted parametr metody
public function show(
ServerRequestInterface $request,
CategoryModel $category
): ResponseInterface
Framework automatycznie:
- Wyodrębnia wartość
categoryz URL (np./categories/5) - Wywołuje
CategoryModel::findForRoute(5) - Wstrzykuje znaleziony model do metody kontrolera
- W przypadku braku modelu zwraca 404
Implementacja findForRoute w modelu
<?php
class Category extends Model
{
public function findForRoute(string|int $value): static
{
return self::query()->select()->where('id', '=', $value)->first();
}
}
Najlepsze praktyki
- Jeden kontroler powinien obsługiwać jeden zasób (np. RegisterController tylko rejestrację) - zgodne z SOLID
- Używaj DTO do walidacji zamiast ręcznej walidacji w kontrolerze
- Wykorzystuj type hints dla automatycznego bindowania modeli
- Oddeleguj złożoną logikę biznesową do serwisów
- Zwracaj odpowiednie kody HTTP (200, 201, 303, 404, etc.)
- Używaj atrybutów do definiowania tras dla lepszej czytelności
🎓 Zbuduj kompletny system kontrolerów!
W kursie PHP 8.4 implementujesz pełny system kontrolerów z automatyczną rejestracją tras, bindowaniem modeli, walidacją przez DTO i middleware. Poznasz wzorce projektowe w praktyce - od Front Controller przez Active Record po Dependency Injection.