MFormations
Modern PHP Engineering

Chapitre 21

21 — Résumés de Livres PHP

21 — Résumés de Livres PHP

Cours 21 — Livres PHP

21.1 PHP 8 Objects, Patterns, and Practice — Matt Zandstra

Présentation

Titre : PHP 8 Objects, Patterns, and Practice Auteur : Matt Zandstra Éditeur : Apress Édition : 6e édition (2021), couvre PHP 8.0/8.1 Pages : ~850 Site : https://www.apress.com/gp/book/9781484267904

Résumé par Chapitre

Partie 1 : Introduction à l'OOP

Le livre commence par une introduction progressive à la programmation orientée objet en PHP.

<?php

// Chapitre 2 : Les bases de l'OOP
class ShopProduct
{
    public function __construct(
        readonly public string $title,
        readonly public string $producerFirstName,
        readonly public string $producerLastName,
        readonly public float $price
    ) {}

    public function getProducer(): string
    {
        return "{$this->producerFirstName} {$this->producerLastName}";
    }

    public function getSummaryLine(): string
    {
        return "{$this->title} par {$this->getProducer()}";
    }
}

// PHP 8.0 : Constructor promotion
// PHP 8.1 : Readonly properties

Partie 2 : Héritage et Polymorphisme

<?php

class BookProduct extends ShopProduct
{
    public function __construct(
        string $title,
        string $producerFirstName,
        string $producerLastName,
        float $price,
        readonly public int $numPages,
    ) {
        parent::__construct(
            $title,
            $producerFirstName,
            $producerLastName,
            $price,
        );
    }

    public function getSummaryLine(): string
    {
        return parent::getSummaryLine()
            . " — {$this->numPages} pages";
    }
}

// Le polymorphisme via les interfaces
interface Chargeable
{
    public function getPrice(): float;
}

class ShopProduct implements Chargeable
{
    public function getPrice(): float
    {
        return $this->price;
    }
}

Partie 3 : Traits et Mixins

<?php

trait PriceUtilities
{
    private static float $taxRate = 0.2;

    public function calculateTax(float $price): float
    {
        return $price * self::$taxRate;
    }

    abstract public function getPrice(): float;
}

class ShopProduct
{
    use PriceUtilities;
}

// Priorité : méthode classe > trait > parent

Partie 4 : Réflexion et Attributs

<?php

// PHP 8.0 : Attributs natifs
#[Attribute]
class JsonSerializableAttribute
{
    public function __construct(
        public readonly array $fields
    ) {}
}

class User
{
    #[JsonSerializableAttribute(['name', 'email'])]
    public function toArray(): array
    {
        return ['name' => $this->name, 'email' => $this->email];
    }
}

// Réflexion
$reflection = new ReflectionClass(User::class);
$attributes = $reflection
    ->getMethod('toArray')
    ->getAttributes(JsonSerializableAttribute::class);

foreach ($attributes as $attribute) {
    $instance = $attribute->newInstance();
    // $instance->fields
}

Partie 5 : Design Patterns

Le livre couvre les patterns GoF classiques adaptés à PHP.

<?php

// Singleton
final class Preferences
{
    private static ?Preferences $instance = null;
    private array $data = [];

    private function __construct() {}

    public static function getInstance(): self
    {
        if (self::$instance === null) {
            self::$instance = new self();
        }
        return self::$instance;
    }

    public function set(string $key, mixed $value): void
    {
        $this->data[$key] = $value;
    }

    public function get(string $key, mixed $default = null): mixed
    {
        return $this->data[$key] ?? $default;
    }
}

// Factory Method
abstract class ApptEncoder
{
    abstract public function encode(): string;
}

class BloggsApptEncoder extends ApptEncoder
{
    public function encode(): string
    {
        return "Bloggs format encodé\n";
    }
}

abstract class CommsManager
{
    abstract public function getApptEncoder(): ApptEncoder;
}

class BloggsCommsManager extends CommsManager
{
    public function getApptEncoder(): ApptEncoder
    {
        return new BloggsApptEncoder();
    }
}

// Composite
abstract class Unit
{
    abstract public function bombardStrength(): int;

    public function addUnit(Unit $unit): void
    {
        throw new \Exception(get_class($this) . ' ne supporte pas addUnit()');
    }
}

class Archer extends Unit
{
    public function bombardStrength(): int
    {
        return 4;
    }
}

class Army extends Unit
{
    private array $units = [];

    public function addUnit(Unit $unit): void
    {
        $this->units[] = $unit;
    }

    public function bombardStrength(): int
    {
        $strength = 0;
        foreach ($this->units as $unit) {
            $strength += $unit->bombardStrength();
        }
        return $strength;
    }
}

Partie 6 : Stratégies de Test

<?php

use PHPUnit\Framework\TestCase;

class ShopProductTest extends TestCase
{
    public function testProductHasTitle(): void
    {
        $product = new ShopProduct('Titre', 'Prénom', 'Nom', 29.99);
        $this->assertEquals('Titre', $product->title);
    }

    public function testBookProductHasPages(): void
    {
        $book = new BookProduct('Titre', 'Prénom', 'Nom', 29.99, 500);
        $this->assertEquals(500, $book->numPages);
    }
}

Partie 7 : Architecture et Intégration

<?php

// Registry Pattern
class Registry
{
    private static array $services = [];

    public static function set(string $key, object $service): void
    {
        self::$services[$key] = $service;
    }

    public static function get(string $key): ?object
    {
        return self::$services[$key] ?? null;
    }
}

// Front Controller
class FrontController
{
    public function handle(Request $request): Response
    {
        $router = Registry::get('router');
        $route = $router->match($request);

        $controller = new $route->controller();
        $method = $route->method;

        return $controller->$method($request);
    }
}

21.2 Laravel: Up & Running — Matt Stauffer

Présentation

Titre : Laravel: Up & Running — A Framework for Building Modern PHP Apps Auteur : Matt Stauffer Éditeur : O'Reilly Édition : 3e édition (2023), couvre Laravel 10 Pages : ~550 Site : https://laravelupandrunning.com

Résumé par Chapitre

Partie 1 : Fondamentaux

<?php

// Chapitre 1 : Introduction au cycle de vie Laravel
// - Service Container
// - Service Providers
// - Facades
// - Helpers

// Exemple : Service Container
app()->bind('PaymentGateway', function ($app) {
    return new StripePaymentGateway(
        config('services.stripe.secret')
    );
});

$gateway = app('PaymentGateway');

// Facade
Payment::charge(100);

// Helper
$user = User::find(1);
return redirect()->route('dashboard');

Partie 2 : Routing et Contrôleurs

<?php

// Chapitre 2-4 : Routing

// Route basique
Route::get('/users', [UserController::class, 'index']);

// Route avec paramètres
Route::get('/users/{user}', [UserController::class, 'show']);

// Route model binding implicite
// User $user sera automatiquement résolu depuis l'ID
Route::get('/users/{user}', function (User $user) {
    return $user;
});

// Route avec validation
Route::get('/posts/{post:slug}', [PostController::class, 'show']);

// Groupes de routes
Route::middleware(['auth', 'verified'])->group(function () {
    Route::resource('posts', PostController::class);
    Route::resource('comments', CommentController::class)->except(['show']);
});

// Contrôleur resource
php artisan make:controller PostController --resource

// Single action controller
class ShowUserProfileController
{
    public function __invoke(User $user): View
    {
        return view('users.profile', ['user' => $user]);
    }
}

Partie 3 : Blade

{{-- Chapitre 5 : Blade Templating --}}

{{-- Layout --}}
@extends('layouts.app')

{{-- Section --}}
@section('content')
    <h1>{{ $title }}</h1>
@endsection

{{-- Composants --}}
<x-card class="mb-4">
    <x-slot:title>
        {{ $post->title }}
    </x-slot>
    <p>{{ $post->excerpt }}</p>
</x-card>

{{-- Attributs du composant --}}
@props(['type' => 'info', 'message'])

<div {{ $attributes->class(['alert', 'alert-' . $type]) }}>
    {{ $message }}
</div>

{{-- Boucles --}}
@foreach ($posts as $post)
    <div>{{ $post->title }}</div>
@endforeach

{{-- Condition --}}
@auth
    <p>Connecté</p>
@endauth

{{-- Raw PHP --}}
@php
    $counter = 0;
@endphp

Partie 4 : Eloquent ORM

<?php

// Chapitre 6-7 : Eloquent

// Relations
class User extends Authenticatable
{
    // One to One
    public function profile(): HasOne
    {
        return $this->hasOne(Profile::class);
    }

    // One to Many
    public function posts(): HasMany
    {
        return $this->hasMany(Post::class);
    }

    // Many to Many
    public function roles(): BelongsToMany
    {
        return $this->belongsToMany(Role::class)
            ->withTimestamps()
            ->withPivot('expires_at');
    }

    // Has Many Through
    public function comments(): HasManyThrough
    {
        return $this->hasManyThrough(
            Comment::class,
            Post::class,
            'user_id',    // Clé étrangère dans posts
            'post_id',     // Clé étrangère dans comments
            'id',          // Clé locale users
            'id'           // Clé locale posts
        );
    }
}

// Accessors et Mutators
class User extends Authenticatable
{
    // Ancienne syntaxe
    public function getNameAttribute($value): string
    {
        return ucfirst($value);
    }

    // Nouvelle syntaxe (Laravel 10+)
    protected function fullName(): Attribute
    {
        return Attribute::make(
            get: fn ($value, $attributes) => 
                "{$attributes['first_name']} {$attributes['last_name']}",
            set: fn ($value) => explode(' ', $value, 2),
        );
    }

    // Casts
    protected function casts(): array
    {
        return [
            'email_verified_at' => 'datetime',
            'is_admin' => 'boolean',
            'config' => 'array',
            'options' => AsEnumCollection::of(UserOption::class),
        ];
    }
}

Partie 5 : Tests

<?php

// Chapitre 11 : Testing

use Pest\Laravel;

// Feature test
it('can list posts', function () {
    Post::factory()->count(3)->create();

    $response = $this->get('/posts');

    $response->assertOk();
    $response->assertJsonCount(3);
});

// Database test
it('creates a user', function () {
    $user = User::factory()->create();

    $this->assertDatabaseHas('users', [
        'id' => $user->id,
        'email' => $user->email,
    ]);
});

// HTTP test avec authentification
it('requires authentication', function () {
    $this->get('/dashboard')
        ->assertRedirect('/login');
});

it('allows authenticated users', function () {
    $this->actingAs(User::factory()->create())
        ->get('/dashboard')
        ->assertOk();
});

21.3 Clean Code PHP — Principes de Robert C. Martin

Présentation

Concept : Traduction et adaptation des principes de Clean Code de Robert C. Martin (Uncle Bob) pour PHP. Référence : Clean Code — A Handbook of Agile Software Craftsmanship (Robert C. Martin, 2008) Adaptation PHP : Communauté open-source (github.com/jupeter/clean-code-php)

Principes Fondamentaux

<?php

// 1. Noms significatifs
// ❌ Mauvais
$d = 24; // jours
$list = ['admin', 'user'];

function calc(int $a, int $b): int
{
    return $a * $b;
}

// ✅ Bon
$expirationInDays = 24;
$roles = ['admin', 'user'];

function calculateArea(int $width, int $height): int
{
    return $width * $height;
}

Règles de nommage :

<?php

// 2. Noms de classes : noms (User) ou syntagmes nominaux (UserRepository)
class User {}
class PaymentGateway {}
class AuthenticationService {}

// 3. Noms de méthodes : verbes ou syntagmes verbaux
$user->getName();
$payment->process();
$auth->authenticate($credentials);

// 4. Noms de variables : noms explicites
$totalPrice = $quantity * $unitPrice;
$isActive = $user->status === 'active';

// 5. Noms de constantes : UPPER_SNAKE_CASE
const MAX_LOGIN_ATTEMPTS = 5;
const DEFAULT_PAGE_SIZE = 50;

// 6. Éviter les abréviations
// ❌ Banni
$usrMgr = new UserManager();
$cfg = config('app');

// ✅ Recommandé
$userManager = new UserManager();
$config = config('app');

Fonctions

<?php

// 1. Fonctions courtes (max 20 lignes)
// ❌ Trop longue
function processOrder(Order $order): void
{
    // Valider le panier (10 lignes)
    // Calculer les taxes (15 lignes)
    // Appliquer les remises (10 lignes)
    // Créer la commande (8 lignes)
    // Envoyer la confirmation (12 lignes)
}

// ✅ Découpée
function processOrder(Order $order): void
{
    $this->validateCart($order->cart);
    $total = $this->calculateTotal($order);
    $this->createOrder($order, $total);
    $this->sendConfirmation($order);
}

// 2. Principe de responsabilité unique
function calculateTotal(Order $order): Money
{
    $subtotal = $this->calculateSubtotal($order->items);
    $tax = $this->calculateTax($subtotal);
    $discount = $this->applyDiscounts($order->coupon, $subtotal);

    return $subtotal->add($tax)->subtract($discount);
}

// 3. Paramètres (max 3, utiliser un objet au-delà)
// ❌ Trop de paramètres
function createUser(
    string $name,
    string $email,
    string $password,
    string $role,
    bool $isActive,
    ?string $phone = null
): User {}

// ✅ Objet DTO
class CreateUserData
{
    public function __construct(
        readonly public string $name,
        readonly public string $email,
        readonly public string $password,
        readonly public Role $role = Role::User,
        readonly public bool $isActive = true,
        readonly public ?string $phone = null,
    ) {}
}

function createUser(CreateUserData $data): User {}

Commentaires

<?php

// 1. Privilégier un code expressif aux commentaires
// ❌ Commentaire superflu
// Vérifie si l'utilisateur est admin
if ($user->role === 'admin') {
    // ...
}

// ✅ Code expressif
if ($user->isAdmin()) {
    // ...
}

// 2. Commentaires légitimes
// TODO: Migrer vers Stripe API v3 d'ici juin
// REGEX: Capture les UUIDs v4
$pattern = '/[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}/i';

// 3. PHPDoc pour les API publiques
/**
 * Traite un paiement via Stripe.
 *
 * @param  PositiveInteger  $amount  Montant en centimes
 * @param  NonEmptyString   $token   Token Stripe généré côté client
 * @return Payment                    Payment result
 *
 * @throws PaymentFailedException Si le paiement est refusé
 */
public function charge(PositiveInteger $amount, NonEmptyString $token): Payment
{
    // ...
}

Formatage

<?php

// 1. Indentation cohérente (PSR-12 : 4 espaces)
class OrderController extends Controller
{
    public function store(OrderRequest $request): JsonResponse
    {
        $validated = $request->validated();

        $order = DB::transaction(function () use ($validated) {
            $order = Order::create($validated);
            $order->items()->createMany($validated['items']);

            return $order;
        });

        return response()->json($order, 201);
    }
}

// 2. Séparation des concepts
class PaymentService
{
    public function __construct(
        private PaymentGateway $gateway,
        private Logger $logger,
    ) {}

    // Ligne vide entre les méthodes
    public function process(Payment $payment): Result
    {
        // ...
    }

    public function refund(Payment $payment): Result
    {
        // ...
    }
}

Gestion d'Erreurs

<?php

// 1. Utiliser les exceptions, pas les codes d'erreur
// ❌ Mauvais
function saveUser(array $data): array
{
    if (empty($data['email'])) {
        return ['error' => 'Email requis', 'code' => 422];
    }
    // ...
}

// ✅ Bon
function saveUser(array $data): User
{
    throw_if(
        empty($data['email']),
        ValidationException::withMessages(['email' => 'Requis'])
    );
}

// 2. Exceptions métier
class InsufficientFundsException extends \RuntimeException
{
    public function __construct(
        public readonly Money $balance,
        public readonly Money $required,
    ) {
        parent::__construct(
            "Fonds insuffisants : {$balance} requis : {$required}"
        );
    }
}

// 3. Try-catch au niveau approprié
class PaymentController extends Controller
{
    public function charge(ChargeRequest $request): JsonResponse
    {
        try {
            $payment = $this->paymentService->charge(
                $request->amount(),
                $request->token(),
            );

            return response()->json($payment);
        } catch (InsufficientFundsException $e) {
            return response()->json([
                'error' => 'Solde insuffisant',
                'balance' => $e->balance,
            ], 402);
        } catch (PaymentFailedException $e) {
            Log::error('Paiement échoué', [
                'exception' => $e,
                'request' => $request->all(),
            ]);

            return response()->json([
                'error' => 'Paiement refusé',
            ], 400);
        }
    }
}

Classes et Objets

<?php

// 1. Principe de Responsabilité Unique (SRP)
// ❌ Classe qui fait trop de choses
class User
{
    public function save(): void {}
    public function sendEmail(): void {}
    public function generateReport(): void {}
    public function calculatePermissions(): void {}
}

// ✅ Classes séparées
class User extends Authenticatable {}
class UserMailer
{
    public function sendWelcome(User $user): void {}
}
class ReportGenerator
{
    public function generate(User $user): Report {}
}
class PermissionCalculator
{
    public function calculate(User $user): PermissionCollection {}
}

// 2. Loi de Déméter (ne pas enchaîner les appels)
// ❌ Violation
$order->getCustomer()->getAddress()->getCity();

// ✅ Correction
$order->getCustomerCity();

// 3. Injection de dépendances
// ❌ Couplage fort
class OrderProcessor
{
    private StripeGateway $gateway;

    public function __construct()
    {
        $this->gateway = new StripeGateway('sk_test_...');
    }
}

// ✅ Découplé
class OrderProcessor
{
    public function __construct(
        private PaymentGateway $gateway,
    ) {}
}

21.4 Patterns of Enterprise Application Architecture — Martin Fowler

Présentation

Titre : Patterns of Enterprise Application Architecture Auteur : Martin Fowler Éditeur : Addison-Wesley Édition : 1ère édition (2002) Pages : ~560 Site : https://martinfowler.com/books/eaa.html

Résumé des Patterns Clés

Patterns de Domain Logic :

<?php

// 1. Transaction Script
class OrderService
{
    public function placeOrder(array $items, string $userId): Order
    {
        $total = Money::from(0);

        foreach ($items as $item) {
            $product = Product::findOrFail($item['product_id']);
            $total = $total->add($product->price->multiply($item['quantity']));
        }

        $order = Order::create([
            'user_id' => $userId,
            'total' => $total,
            'status' => OrderStatus::Pending,
        ]);

        foreach ($items as $item) {
            $order->items()->create($item);
        }

        event(new OrderPlaced($order));

        return $order;
    }
}

// 2. Domain Model
class Money
{
    public function __construct(
        readonly public int $amount,    // en centimes
        readonly public string $currency = 'EUR',
    ) {}

    public function add(Money $other): Money
    {
        $this->assertSameCurrency($other);
        return new self($this->amount + $other->amount, $this->currency);
    }

    public function multiply(int $quantity): Money
    {
        return new self($this->amount * $quantity, $this->currency);
    }

    public function isGreaterThan(Money $other): bool
    {
        $this->assertSameCurrency($other);
        return $this->amount > $other->amount;
    }

    private function assertSameCurrency(Money $other): void
    {
        if ($this->currency !== $other->currency) {
            throw new CurrencyMismatchException($this->currency, $other->currency);
        }
    }
}

// 3. Table Module
class OrderTableModule
{
    public function __construct(
        private DB $db,
    ) {}

    public function getTotalForUser(int $userId): Money
    {
        $rows = $this->db->query(
            'SELECT SUM(total) as total FROM orders WHERE user_id = ?',
            [$userId]
        );

        return Money::from($rows[0]['total']);
    }
}

Patterns de Data Source :

<?php

// 1. Table Data Gateway
class UserGateway
{
    public function __construct(
        private DB $db,
    ) {}

    public function find(int $id): ?array
    {
        $row = $this->db->query(
            'SELECT * FROM users WHERE id = ?',
            [$id]
        );

        return $row[0] ?? null;
    }

    public function insert(array $data): int
    {
        return $this->db->insert('users', $data);
    }

    public function update(int $id, array $data): void
    {
        $this->db->update('users', $data, ['id' => $id]);
    }

    public function delete(int $id): void
    {
        $this->db->delete('users', ['id' => $id]);
    }
}

// 2. Row Data Gateway
class UserRowGateway
{
    public function __construct(
        private DB $db,
        public int $id,
        public string $name,
        public string $email,
    ) {}

    public function save(): void
    {
        $this->db->update('users', [
            'name' => $this->name,
            'email' => $this->email,
        ], ['id' => $this->id]);
    }

    public function delete(): void
    {
        $this->db->delete('users', ['id' => $this->id]);
    }
}

// 3. Active Record (Eloquent)
$user = User::find(1);
$user->name = 'John Doe';
$user->save();

// 4. Data Mapper (Doctrine)
$entityManager->persist($user);
$entityManager->flush();

Patterns de Concurrency :

<?php

// 1. Optimistic Offline Lock
#[Entity]
class Article
{
    #[Version]
    private int $version;

    public function update(string $content): void
    {
        // Vérification automatique par Doctrine
        // UPDATE article SET content = ?, version = version + 1
        // WHERE id = ? AND version = ?
        // Si version mismatch → OptimisticLockException
        $this->content = $content;
    }
}

// 2. Pessimistic Offline Lock
class PessimisticLock
{
    private static array $locks = [];

    public function acquire(string $resourceId, string $userId): bool
    {
        $lock = Lock::where('resource_id', $resourceId)->first();

        if ($lock && $lock->user_id !== $userId) {
            return false; // Ressource verrouillée par un autre
        }

        Lock::updateOrCreate(
            ['resource_id' => $resourceId],
            ['user_id' => $userId, 'acquired_at' => now()]
        );

        return true;
    }

    public function release(string $resourceId): void
    {
        Lock::where('resource_id', $resourceId)->delete();
    }
}

Patterns de Session :

<?php

// 1. Client Session State
session_start();
$_SESSION['cart'] = [
    ['product_id' => 1, 'quantity' => 2],
    ['product_id' => 3, 'quantity' => 1],
];

// 2. Server Session State
// Laravel : sessions en base de données, Redis, fichier
Config::set('session.driver', 'redis');

// 3. Database Session State
// Table sessions : id, payload, last_activity, user_id
Schema::create('sessions', function (Blueprint $table) {
    $table->string('id')->primary();
    $table->foreignId('user_id')->nullable();
    $table->text('payload');
    $table->integer('last_activity');
});

21.5 The Clean Architecture — Robert C. Martin

Présentation

Titre : Clean Architecture — A Craftsman's Guide to Software Structure and Design Auteur : Robert C. Martin (Uncle Bob) Éditeur : Prentice Hall Édition : 1ère édition (2017) Pages : ~432 Site : https://www.oreilly.com/library/view/clean-architecture-a/9780134494272/

Application en PHP/Laravel

Les 4 Couches :

<?php

// === COUCHE ENTREPRISE (Enterprise Business Rules) ===
// Entities — Objets métier purs, sans dépendance framework

namespace Domain\Entities;

use Domain\ValueObjects\Email;
use Domain\ValueObjects\UserId;

class User
{
    public function __construct(
        private UserId $id,
        private string $name,
        private Email $email,
        private \DateTimeImmutable $registeredAt,
    ) {}

    public function changeEmail(Email $newEmail): void
    {
        if ($this->email->equals($newEmail)) {
            throw new \DomainException('Le nouvel email est identique à l\'actuel');
        }
        $this->email = $newEmail;
    }
}

// Value Objects
namespace Domain\ValueObjects;

class Email
{
    private string $value;

    public function __construct(string $value)
    {
        if (!filter_var($value, FILTER_VALIDATE_EMAIL)) {
            throw new \InvalidArgumentException("Email invalide: $value");
        }
        $this->value = $value;
    }

    public function value(): string
    {
        return $this->value;
    }

    public function equals(Email $other): bool
    {
        return $this->value === $other->value;
    }
}
<?php

// === COUCHE APPLICATION (Application Business Rules) ===
// Use Cases — Cas d'utilisation orchestrant les entities

namespace Application\UseCases;

use Domain\Entities\User;
use Domain\Repositories\UserRepositoryInterface;
use Domain\ValueObjects\Email;
use Domain\ValueObjects\UserId;

class RegisterUserUseCase
{
    public function __construct(
        private UserRepositoryInterface $userRepository,
    ) {}

    public function execute(RegisterUserRequest $request): RegisterUserResponse
    {
        $email = new Email($request->email);

        if ($this->userRepository->findByEmail($email)) {
            throw new UserAlreadyExistsException($email);
        }

        $user = new User(
            UserId::generate(),
            $request->name,
            $email,
            new \DateTimeImmutable(),
        );

        $this->userRepository->save($user);

        return new RegisterUserResponse($user);
    }
}

// DTOs d'entrée/sortie
class RegisterUserRequest
{
    public function __construct(
        public readonly string $name,
        public readonly string $email,
        public readonly string $password,
    ) {}
}

class RegisterUserResponse
{
    public function __construct(
        public readonly User $user,
    ) {}
}
<?php

// === COUCHE INTERFACE ADAPTERS ===
// Controllers, Presenters, Gateways

namespace Infrastructure\Http\Controllers\Api;

use Application\UseCases\RegisterUserUseCase;
use Application\UseCases\RegisterUserRequest;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;

class UserController
{
    public function __construct(
        private RegisterUserUseCase $registerUser,
    ) {}

    public function store(Request $request): JsonResponse
    {
        $useCaseRequest = new RegisterUserRequest(
            name: $request->input('name'),
            email: $request->input('email'),
            password: $request->input('password'),
        );

        try {
            $response = $this->registerUser->execute($useCaseRequest);

            return response()->json([
                'id' => (string) $response->user->getId(),
                'name' => $response->user->getName(),
                'email' => $response->user->getEmail()->value(),
            ], 201);
        } catch (\DomainException $e) {
            return response()->json([
                'error' => $e->getMessage(),
            ], 409);
        }
    }
}
<?php

// === COUCHE FRAMEWORK / INFRASTRUCTURE ===
// Database, Queue, Mail, External Services

namespace Infrastructure\Persistence\Eloquent;

use App\Models\User as EloquentUser;
use Domain\Entities\User;
use Domain\Repositories\UserRepositoryInterface;
use Domain\ValueObjects\Email;
use Domain\ValueObjects\UserId;

class EloquentUserRepository implements UserRepositoryInterface
{
    public function save(User $user): void
    {
        EloquentUser::updateOrCreate(
            ['id' => (string) $user->getId()],
            [
                'name' => $user->getName(),
                'email' => $user->getEmail()->value(),
                'registered_at' => $user->getRegisteredAt(),
            ]
        );
    }

    public function findByEmail(Email $email): ?User
    {
        $eloquentUser = EloquentUser::where('email', $email->value())->first();

        if (!$eloquentUser) {
            return null;
        }

        return $this->toDomainEntity($eloquentUser);
    }

    private function toDomainEntity(EloquentUser $eloquent): User
    {
        return new User(
            new UserId($eloquent->id),
            $eloquent->name,
            new Email($eloquent->email),
            new \DateTimeImmutable($eloquent->registered_at),
        );
    }
}

Services Providers pour l'Injection

<?php

namespace App\Providers;

use Domain\Repositories\UserRepositoryInterface;
use Illuminate\Support\ServiceProvider;
use Infrastructure\Persistence\Eloquent\EloquentUserRepository;

class CleanArchitectureServiceProvider extends ServiceProvider
{
    public function register(): void
    {
        $this->app->bind(
            UserRepositoryInterface::class,
            EloquentUserRepository::class,
        );

        $this->app->bind(RegisterUserUseCase::class, function ($app) {
            return new RegisterUserUseCase(
                $app->make(UserRepositoryInterface::class),
            );
        });
    }
}

Tests avec Clean Architecture

<?php

namespace Tests\Unit\Application\UseCases;

use Application\UseCases\RegisterUserUseCase;
use Application\UseCases\RegisterUserRequest;
use Domain\Entities\User;
use Domain\Repositories\UserRepositoryInterface;
use Domain\ValueObjects\Email;
use PHPUnit\Framework\TestCase;

class RegisterUserUseCaseTest extends TestCase
{
    public function test_can_register_user(): void
    {
        $repository = $this->createMock(UserRepositoryInterface::class);
        $repository
            ->expects($this->once())
            ->method('findByEmail')
            ->willReturn(null);

        $repository
            ->expects($this->once())
            ->method('save')
            ->with($this->isInstanceOf(User::class));

        $useCase = new RegisterUserUseCase($repository);

        $request = new RegisterUserRequest(
            name: 'John Doe',
            email: 'john@example.com',
            password: 'secret123',
        );

        $response = $useCase->execute($request);

        $this->assertInstanceOf(User::class, $response->user);
        $this->assertEquals('john@example.com', $response->user->getEmail()->value());
    }

    public function test_throws_exception_when_email_exists(): void
    {
        $repository = $this->createMock(UserRepositoryInterface::class);
        $repository
            ->expects($this->once())
            ->method('findByEmail')
            ->willReturn($this->createMock(User::class));

        $useCase = new RegisterUserUseCase($repository);

        $this->expectException(\DomainException::class);

        $useCase->execute(new RegisterUserRequest(
            name: 'John Doe',
            email: 'existing@example.com',
            password: 'secret123',
        ));
    }
}

21.6 Lectures Complémentaires

Design Patterns en PHP

<?php

// Strategy Pattern
interface ExportStrategy
{
    public function export(array $data): string;
}

class CsvExport implements ExportStrategy
{
    public function export(array $data): string
    {
        $output = fopen('php://temp', 'r+');
        foreach ($data as $row) {
            fputcsv($output, $row);
        }
        rewind($output);
        return stream_get_contents($output);
    }
}

class JsonExport implements ExportStrategy
{
    public function export(array $data): string
    {
        return json_encode($data, JSON_THROW_ON_ERROR | JSON_PRETTY_PRINT);
    }
}

class XmlExport implements ExportStrategy
{
    public function export(array $data): string
    {
        $xml = new SimpleXMLElement('<root/>');
        foreach ($data as $key => $value) {
            $xml->addChild($key, is_array($value)
                ? json_encode($value)
                : $value
            );
        }
        return $xml->asXML();
    }
}

class Exporter
{
    public function __construct(
        private ExportStrategy $strategy,
    ) {}

    public function export(array $data): string
    {
        return $this->strategy->export($data);
    }
}

// Usage
$exporter = new Exporter(new JsonExport());
$json = $exporter->export($usersData);

Enterprise Integration Patterns

<?php

// Message Pattern (via Laravel Queues)
class ProcessPaymentJob implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    public function __construct(
        private Order $order,
    ) {}

    public function handle(PaymentService $payment): void
    {
        $result = $payment->charge($this->order);

        if ($result->failed) {
            $this->release(60); // Retry after 60s
            return;
        }

        $this->order->markAsPaid();
    }

    public function failed(\Throwable $e): void
    {
        Log::critical('Paiement définitivement échoué', [
            'order_id' => $this->order->id,
            'error' => $e->getMessage(),
        ]);

        $this->order->markAsFailed();
        event(new PaymentFailed($this->order));
    }
}