Kontroler w MVC - Na Przykładzie Kontrolera Rejestracji

15.06.2025

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:

  1. Tworzy instancję DTO
  2. Odczytuje atrybuty walidacyjne (#[Required], #[Email], etc.)
  3. Wykonuje walidację danych z żądania HTTP
  4. W przypadku błędów zwraca odpowiedź JSON z komunikatami
  5. 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:

  1. Wyodrębnia wartość category z URL (np. /categories/5)
  2. Wywołuje CategoryModel::findForRoute(5)
  3. Wstrzykuje znaleziony model do metody kontrolera
  4. 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.

Kup pełny kurs PHP 8.4 🚀 Pobierz darmowy fragment 📥

Zobacz powiązane artykuły