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