MVC w PHP - Model-View-Controller w Praktyce

30.09.2025

MVC w PHP - Model-View-Controller w Praktyce

Czym jest architektura MVC?

Model-View-Controller to klasyczny wzorzec architektoniczny dzielący aplikację na trzy współpracujące ze sobą komponenty. Każdy z nich ma ściśle określoną rolę, co pozwala na przejrzystą strukturę i łatwiejsze zarządzanie kodem.

Podział odpowiedzialności w MVC

  • Model - Reprezentuje dane oraz reguły biznesowe aplikacji
  • View - Odpowiada za wizualizację danych dla użytkownika końcowego
  • Controller - Przetwarza żądania użytkownika i koordynuje pracę modelu oraz widoku

Zalety architektury MVC w praktyce

Co zyskujesz stosując ten wzorzec?

  • Modularność kodu - Każdy komponent można modyfikować niezależnie
  • Łatwe utrzymanie - Zmiany w jednej warstwie nie wpływają na pozostałe
  • Praca zespołowa - Frontend i backend mogą być rozwijane równolegle
  • Testowalność - Izolowane warstwy ułatwiają pisanie testów jednostkowych
  • Elastyczność - Możliwość podmiany implementacji bez zmian w całym systemie

Model - Zarządzanie danymi w PHP 8.4

Warstwa modelu zajmuje się obsługą danych i implementacją logiki biznesowej. Dzięki wykorzystaniu Property Hooks dostępnych w PHP 8.4, możliwe jest automatyczne wykrywanie zmian w obiektach i generowanie optymalnych zapytań do bazy.

Active Record z nowościami PHP 8.4

<?php
class User extends Model
{
    public string $table { get => 'users'; }

    // Property Hook - automatyczne śledzenie zmian
    public string $name {
        get => $this->name;
        set {
            $this->name = $value;
            $this->markPropertyAsChanged('name');
        }
    }

    public string $email {
        get => $this->email;
        set {
            $this->email = $value;
            $this->markPropertyAsChanged('email');
        }
    }

    // Relacje
    #[HasMany(Post::class, foreign_key: 'user_id')]
    public array $posts {
        get => $this->relations->getRelation('posts');
    }
}

// Tworzenie i aktualizacja
$user = new User();
$user->name = 'Jan Kowalski';
$user->email = 'jan@example.com';
$user->save(); // INSERT

$user->name = 'Janina Kowalska';
$user->save(); // UPDATE - tylko zmienione pola

// Relacje
$userPosts = $user->posts; // Automatyczne pobranie powiązanych wpisów

Fluent Query Builder

Do budowy zapytań służy obiektowy interfejs łańcuchowy, który zapewnia czytelność i bezpieczeństwo przed atakami SQL Injection dzięki automatycznemu parametryzowaniu wartości.

<?php
// Zapytanie z łączeniem tabel
$activeUsers = User::query()
    ->select('users')
    ->leftJoin('posts', 'users.id', '=', 'posts.user_id')
    ->where('users.active', '=', 1)
    ->whereNotNull('users.email')
    ->orderBy('users.created_at', 'DESC')
    ->limit(10)
    ->get();

View - Rendering interfejsu użytkownika

Warstwa widoków odpowiada za prezentację danych. Możesz wybrać spośród trzech różnych podejść: tradycyjne szablony Twig, autorski silnik inspirowany Blade lub hybrydowe SPA oparte na Inertia.js z Vue 3.

Widok dziedziczący z layoutu
{% extends "layouts/app.twig" %}

{% block title %}Lista użytkowników{% endblock %}

{% block content %}
    <h1>{{ pageTitle }}</h1>

    <ul>
    {% for user in users %}
        <li>{{ user.name }} - {{ user.email }}</li>
    {% endfor %}
    </ul>
{% endblock %}

2. Autorski silnik szablonów

Możesz zbudować własny parser szablonów z dyrektywami podobnymi do Laravel Blade - wspierający pętle, warunki oraz komponenty wielokrotnego użytku.

@extends('layouts.app')

@section('content')
  <h1>{{ $pageTitle }}</h1>

  @if(count($users) > 0)
    @foreach($users as $user)
      <div >
        <h3>{{ $user->name }}</h3>
        <p>{{ $user->email }}</p>
      </div>
    @endforeach
  @endif
@endsection

3. Hybrydowe SPA z Inertia.js

Inertia.js łączy zalety aplikacji serwerowych i Single Page Apps. Serwer wysyła JSON, a Vue renderuje komponenty po stronie klienta - bez konieczności budowy REST API.

<?php
// Kontroler przekazuje dane do Vue
return Inertia::render('Users/Index.vue', [
    'users' => $users,
    'title' => 'Użytkownicy',
]);

Controller - Obsługa żądań HTTP

Kontrolery stanowią pomost między użytkownikiem a aplikacją. Odbierają żądania HTTP, wywołują odpowiednie metody modelu, a następnie wybierają widok do wyświetlenia wyniku.

Kontroler rejestracji użytkownika

<?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);
    }
}

Walidacja przez Data Transfer Object

Wykorzystanie DTO pozwala na deklaratywną walidację danych wejściowych. Framework sprawdza poprawność danych jeszcze przed przekazaniem ich do kontrolera.

<?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;
}

Route Model Binding

System routingu potrafi automatycznie załadować obiekt z bazy na podstawie parametru URL. Wystarczy oznaczyć parametr odpowiednim atrybutem.

<?php
#[RouteGroup('categories')]
class CategoriesController extends Controller
{
    #[Route('/<category:\d+>', methods: ['GET'])]
    #[RouteParam('category', bind: CategoryModel::class)]
    public function show(CategoryModel $category): ResponseInterface
    {
        // Obiekt $category jest już załadowany z bazy
        return new Response()->withContent(json_encode($category));
    }
}

Kompletny przykład: CRUD użytkowników

Poniżej przedstawiam jak wszystkie warstwy MVC współdziałają w realnym scenariuszu zarządzania użytkownikami.

Warstwa modelu

<?php
class User extends Model
{
    public string $table { get => 'users'; }

    public string $name {
        get => $this->name;
        set {
            $this->name = $value;
            $this->markPropertyAsChanged('name');
        }
    }

    #[HasMany(Post::class, foreign_key: 'user_id')]
    public array $posts { get => $this->relations->getRelation('posts'); }

    public static function findActive(): array
    {
        return self::query()
            ->select()
            ->where('active', '=', 1)
            ->orderBy('created_at', 'DESC')
            ->get();
    }
}

Warstwa kontrolera

<?php
#[RouteGroup('users')]
class UserController extends Controller
{
    #[Route('/', methods: 'GET')]
    public function index(): ResponseInterface
    {
        $users = User::findActive();

        return $this->render('users/index.twig', [
            'title' => 'Użytkownicy',
            'users' => $users,
        ]);
    }

    #[Route('/<user:\d+>', methods: 'GET')]
    #[RouteParam('user', bind: User::class)]
    public function show(User $user): ResponseInterface
    {
        return Inertia::render('Users/Show.vue', [
            'user' => $user,
            'posts' => $user->posts,
        ]);
    }

    #[Route('/', methods: 'POST')]
    public function store(CreateUserDTO $dto): ResponseInterface
    {
        $user = new User()->fill($dto->toArray());
        $user->save();

        return new Response()
            ->withHeader('Location', "/users/{$user->id}")
            ->withStatus(303);
    }
}

Warstwa widoku

{% extends "layouts/app.twig" %}

{% block title %}{{ pageTitle }}{% endblock %}

{% block content %}
  <div >
    <h1>{{ pageTitle }}</h1>

    <a href="/users/create" >Dodaj użytkownika</a>

    <div >
    {% for user in users %}
      <div >
        <h3>{{ user.name }}</h3>
        <p>{{ user.email }}</p>
        <small>Dołączył: {{ user.created_at|date("d-m-Y") }}</small>
        <a href="/users/{{ user.id }}">Zobacz profil</a>
      </div>
    {% else %}
      <p>Brak użytkowników.</p>
    {% endfor %}
    </div>
  </div>
{% endblock %}

Wykorzystane wzorce projektowe

  • Front Controller - Centralizacja obsługi żądań HTTP
  • Active Record - Połączenie danych z operacjami na bazie
  • Template View - Szablony z dynamicznymi danymi
  • Dependency Injection - Wstrzykiwanie zależności przez konstruktor
  • Factory - Tworzenie obiektów przez dedykowane fabryki
  • Facade - Uproszczony interfejs do złożonych systemów

Dobre praktyki przy implementacji MVC

  • Szczupłe kontrolery - Deleguj skomplikowaną logikę do klas serwisowych
  • Zawsze typuj - Type hints ułatwiają debugowanie i autowiring
  • Waliduj przez DTO - Unikaj rozbudowanych metod walidacyjnych w kontrolerach
  • Wykorzystuj atrybuty - PHP 8+ pozwala na deklaratywny routing
  • Standardy PSR - PSR-7 dla HTTP, PSR-11 dla DI, PSR-15 dla middleware
  • Property Hooks - Nowoczesne podejście do getterów/setterów w PHP 8.4

🎓 Stwórz własny framework MVC!

Kurs PHP 8.4 przeprowadzi Cię przez budowę kompletnej architektury MVC - routing z atrybutami, Query Builder, ORM wykorzystujący Property Hooks oraz trzy warianty systemu widoków. Wszystko krok po kroku, z testami i objaśnieniami.

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

Zobacz powiązane artykuły

Zakres merytoryczny kursu

Obszary nauki

  • Podstawy wzorca MVC

  • Zaawansowane techniki implementacji

  • Integracja z bazami danych

  • Projektowanie responsywnych interfejsów

Zdobyte umiejętności

  • Tworzenie profesjonalnych aplikacji PHP

  • Stosowanie architektury wielowarstwowej

  • Zarządzanie złożonymi projektami webowymi

Dla kogo jest ten kurs?

  • Początkujący programiści PHP

  • Developerzy chcący podnieść swoje umiejętności

  • Osoby zainteresowane profesjonalnym podejściem do tworzenia oprogramowania

Technologie i narzędzia

Kurs obejmuje praktyczną naukę:

  • PHP 8.x

  • Frameworków MVC

  • Pracy z bazami danych

  • Narzędzi wspierających development

Korzyści po ukończeniu kursu

  • Umiejętność projektowania skalowalnych aplikacji

  • Zwiększone szanse na rynku pracy

  • Solidna podstawa do dalszego rozwoju

  • Zrozumienie nowoczesnych metodyk tworzenia oprogramowania

Dlaczego warto wybrać nasz kurs?

  • Nastawienie na praktykę

  • Realne projekty

  • Doświadczeni prowadzący

  • Kompleksowe podejście do nauki programowania