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.
{% 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.
Zobacz powiązane artykuły
- Kontroler w MVC - Na Przykładzie Kontrolera Rejestracji
- Widoki w PHP - Kompleksowy Przewodnik po Twig
- System Widoków w PHP - Od Twig przez Blade do Inertia.js
- Bazy Danych w PHP - Kompleksowy Przewodnik po DBAL
- ORM w PHP - Active Record z Property Hooks PHP 8.4
- Nowości w PHP 8.4 - Property Hooks i nowe funkcjonalności
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