MFormations
Modern PHP Engineering

Chapitre 17

17 — Exercices PHP Modernes

17 — Exercices PHP Modernes

Chapitre 17 — Exercices PHP Modernes

17.0 Introduction

Ce chapitre contient 40 exercices progressifs couvrant l'ensemble du syllabus Modern PHP Engineering. Chaque exercice est conçu pour valider une compétence spécifique, de la POO PHP 8 aux architectures modernes.


17.1 Groupe A : OOP PHP 8 (Exercices 1-6)

Exercice 1 : Classes avec readonly properties

Créez une classe User PHP 8 avec :

  • readonly string $uuid
  • readonly string $email
  • readonly DateTimeImmutable $createdAt
  • string $name mutable
  • Un constructeur avec promotion de paramètres
  • Une méthode toArray(): array
class User
{
    public function __construct(
        public readonly string $uuid,
        public readonly string $email,
        public readonly DateTimeImmutable $createdAt,
        public string $name,
    ) {}

    public function toArray(): array
    {
        return [
            'uuid' => $this->uuid,
            'email' => $this->email,
            'name' => $this->name,
            'created_at' => $this->createdAt->format('c'),
        ];
    }
}

Consigne : Ajoutez un Value Object Email qui valide le format et l'utilisez dans User.

Exercice 2 : Enums backed avec méthode statique

Créez un BackedEnum OrderStatus : Pending, Confirmed, Shipped, Delivered, Cancelled.

  • Chaque case a un label en français
  • Méthode statique fromLabel(string $label): self
  • Méthode canTransitionTo(OrderStatus $target): bool (ex: Cancelled → Pending interdit)

Implémentez une classe Order qui utilise cet enum.

Exercice 3 : Pattern Strategy avec interfaces

Système de notification :

  • Interface NotificationChannel avec send(string $to, string $message): bool
  • Implémentez EmailChannel, SMSChannel, SlackChannel
  • Classe NotificationService qui prend un tableau de channels
  • La méthode notifyAll(string $to, string $message): array retourne les résultats

Exercice 4 : Generic-like collections typées

Créez une classe Collection générique en PHP 8 :

  • Support de array<T> avec @template T
  • Méthodes : map(callable): static, filter(callable): static, first(): ?T, toArray(): array
  • Implémentez ArrayAccess, Countable, IteratorAggregate
/**
 * @template T
 */
class Collection implements \ArrayAccess, \Countable, \IteratorAggregate
{
    /** @param array<T> $items */
    public function __construct(private array $items = []) {}

    /** @return Collection<U> */
    public function map(callable $callback): Collection
    {
        return new self(array_map($callback, $this->items));
    }

    /** @return Collection<T> */
    public function filter(callable $callback): Collection
    {
        return new self(array_values(array_filter($this->items, $callback)));
    }

    /** @return ?T */
    public function first(): mixed
    {
        return $this->items[0] ?? null;
    }

    /** @return array<T> */
    public function toArray(): array
    {
        return $this->items;
    }

    public function offsetExists(mixed $offset): bool
    {
        return isset($this->items[$offset]);
    }

    public function offsetGet(mixed $offset): mixed
    {
        return $this->items[$offset] ?? null;
    }

    public function offsetSet(mixed $offset, mixed $value): void
    {
        $offset === null ? $this->items[] = $value : $this->items[$offset] = $value;
    }

    public function offsetUnset(mixed $offset): void
    {
        unset($this->items[$offset]);
    }

    public function count(): int
    {
        return count($this->items);
    }

    public function getIterator(): \ArrayIterator
    {
        return new \ArrayIterator($this->items);
    }
}

Exercice 5 : Value Objects immutables

Créez un Value Object Money :

  • int $cents privé
  • Méthodes : add(Money $other): Money, subtract(Money $other): Money, multiply(float $factor): Money
  • static fromFloat(float $amount, string $currency): self
  • __toString(): string → affiche "12,34 €"
  • Implémentez l'immutabilité (toutes les opérations retournent une nouvelle instance)

Exercice 6 : Active Record vs Data Mapper

Créez deux approches pour un modèle Product :

  1. Active Record : Étendez une classe ActiveRecord qui fait du CRUD via PDO magiquement
  2. Data Mapper : Créez une classe ProductMapper qui transforme entre tableau et objet

Comparez les deux approches en termes de testabilité et couplage.


17.2 Groupe B : PDO & ORM (Exercices 7-12)

Exercice 7 : CRUD avec requêtes préparées PDO

Créez un repository UserRepository avec PDO :

  • findById(int $id): ?User
  • findByEmail(string $email): ?User
  • save(User $user): void (INSERT ou UPDATE)
  • delete(int $id): bool
  • Toutes les méthodes utilisent des requêtes préparées (prepare/execute)
  • Gérez les exceptions PDO

Exercice 8 : Pagination et filtres sécurisés

Implémentez PaginatedQuery :

  • public static function paginate(PDO $pdo, string $baseQuery, array $params, int $page, int $perPage): array
  • Retourne : items, total, page, perPage, lastPage
  • Les paramètres sont bindés proprement via bindValue
  • Échappement des LIKE pour la recherche textuelle

Exercice 9 : Transactions et rollback

Scénario e-commerce : OrderProcessor qui :

  • Crée une commande
  • Décrémente le stock
  • Crée un paiement
  • Envoie une notification
  • Si une étape échoue, tout est rollback
  • Utilise PDO::beginTransaction/commit/rollback

Exercice 10 : Relations Eloquent complexes

Modèles Laravel : Team, User, Role, Project

  • Un Team a plusieurs Users (belongsToMany)
  • Un User a plusieurs Roles (belongsToMany via team_user_role)
  • Un Team a plusieurs Projects (hasMany)
  • Un Project appartient à plusieurs Users (belongsToMany via project_user)
  • Scope : team.active() pour filtrer les équipes actives
  • Implémentez un HasRoles trait

Exercice 11 : Doctrine DQL avancé

Avec Doctrine ORM :

  • Entités : BlogPost, Category, Comment, Author
  • DQL : Pagination avec jointures, agrégation COUNT des commentaires par post
  • Query de recherche plein texte (LIKE sur title et content)
  • QueryBuilder conditionnel (filtres optionnels : category, date, author)

Exercice 12 : Eager/Lazy loading optimization

Analyse du problème N+1 :

  • Créez un seeder avec 100 posts, chacun avec 10 commentaires
  • Comparez le nombre de requêtes avec et sans eager loading
  • Utilisez withCount, loadMissing, cursor pour les gros volumes
  • Benchmark avec DB::enableQueryLog()

17.3 Groupe C : Laravel (Exercices 13-18)

Exercice 13 : Artisan command personnalisée

Créez php artisan user:create :

  • Arguments : {name}, {email}
  • Option : {--admin} pour rôle admin
  • Crée l'utilisateur et envoie un email de bienvenue via notification
  • Affiche un tableau (Table style) avec les détails créés
  • Gère les erreurs (email déjà pris)

Exercice 14 : Validation et Form Request

Créez StoreProductRequest :

  • Règles : name (required, max:255), price (required, numeric, min:0), category_id (exists:categories,id)
  • Messages personnalisés en français
  • after validation hook : vérifie que le prix n'a pas changé depuis la dernière mise à jour
  • Testez avec $this->postJson() dans un test HTTP

Exercice 15 : Events, Listeners, Jobs

Système de notification d'événements :

  • Event : OrderShipped
  • Listeners : SendShipmentNotification, UpdateOrderStatus, LogShipment
  • Job : ProcessCustomerRewards — dispatché 24h après l'expédition
  • Queue : utilise la connection Redis avec queue:work
  • Test : vérifiez que le Listener est bien appelé

Exercice 16 : Policy et Gates

Autorisation sur Post :

  • Policy : view, create, update, delete, restore, forceDelete
  • Gates : admin (role admin), moderate (role moderator)
  • Utilisez @can et @cannot dans Blade
  • Testez avec $this->actingAs($user)->assertCan(), assertCannot()

Exercice 17 : Service Provider personnalisé

Créez PaymentServiceProvider :

  • Bind : PaymentGateway::classStripeGateway::class
  • Singleton : CurrencyConverter::class
  • Deferrable si possible
  • Publiez la config config/payments.php
  • Ajoutez une facade Payment avec @method static string charge(int $amount, string $token)

Exercice 18 : Multi-tenancy avec scoping

Implémentez le multi-tenancy par scope :

  • Trait BelongsToTenant sur les modèles
  • Trait HasTenants sur le modèle Team
  • Middleware IdentifyTenant qui set le tenant_id dans la session
  • Global scope TenantScope qui filtre automatiquement
  • Migration : ajoutez tenant_id aux tables concernées

17.4 Groupe D : API REST (Exercices 19-23)

Exercice 19 : CRUD API avec Sanctum

API RESTful pour une ressource Article :

  • Routes protégées par Sanctum (sauf index/show)
  • CRUD complet avec API Resource
  • Pagination avec meta data
  • Tests HTTP Pest complets

Exercice 20 : API Resources et collections

Utilisez ArticleResource et ArticleCollection :

  • Resource : inclut les relations (author, category, tags)
  • Collection : pagination meta, links (first, last, prev, next)
  • Conditionally apply attributes (ex: email visible que si admin)
  • Merging de valeurs optionnelles

Exercice 21 : Rate limiting et Throttle

Middleware de rate limiting :

  • 60 requêtes/min pour les guests
  • 120 requêtes/min pour les utilisateurs authentifiés
  • 300 requêtes/min pour les admins
  • Retourne 429 Too Many Requests avec headers X-RateLimit-*
  • Implémentez un cache driver personnalisé pour Redis

Exercice 22 : Versioning d'API

Implémentez le versioning par header Accept :

  • Accept: application/vnd.app.v1+json
  • Accept: application/vnd.app.v2+json
  • Middleware ApiVersion qui route vers le bon controller
  • Ou namespaces : App\Http\Controllers\Api\V1\, App\Http\Controllers\Api\V2\
  • Stratégie : rétrocompatibilité garantie sur 2 versions

Exercice 23 : Spatie Query Builder

API avancée avec spatie/laravel-query-builder :

  • allowedFilters : name, email, created_at (avec scope personnalisé)
  • allowedSorts : name, -created_at
  • allowedIncludes : posts, roles, teams
  • allowedFields : limiter les colonnes retournées
  • Pagination avec JsonApiPaginate

17.5 Groupe E : Tests Pest (Exercices 24-27)

Exercice 24 : Test unitaire de Value Object

Testez Money Value Object :

  • it can add money
  • it cannot subtract more than available
  • it formats correctly
  • it is immutable

Exercice 25 : Test HTTP d'API

Testez l'API des Articles :

  • it lists articles with pagination
  • it creates an article when authenticated
  • it returns 422 on invalid data
  • it returns 401 when not authenticated
  • Utilisez actingAs(), assertJsonStructure(), assertJsonCount()

Exercice 26 : Test de commande Artisan

Testez la commande user:create :

  • expectsQuestion pour les inputs interactifs
  • assertExitCode(0) pour succès
  • assertDatabaseHas pour vérifier la création
  • Testez le cas où l'email existe déjà

Exercice 27 : Test de notification

Testez OrderShipped notification :

  • assertSentTo vérifie l'envoi
  • assertNotSentTo pour les non-envois
  • assertSentVia vérifie le canal (mail, database)
  • Fake les notifications avec Notification::fake()

17.6 Groupe F : Performance (Exercices 28-30)

Exercice 28 : Configuration OpCache optimale

Analysez et optimisez OpCache :

  • php.ini avec valeurs recommandées
  • Script de monitoring opcache_get_status()
  • Preloading avec opcache.preload pour Laravel/Symfony
  • Comparaison avant/après avec microtime

Exercice 29 : JIT Tuning PHP 8.4

Optimisez le JIT :

  • opcache.jit=tracing
  • opcache.jit_buffer_size=256M
  • Script benchmark comparant JIT on/off
  • Analyse des fonctions qui bénéficient du JIT (calcul intensif vs I/O)
  • jit_profile et jit_debug pour le profilage

Exercice 30 : Profiling avec Xdebug

Profilez une application Laravel :

  • Xdebug profiler activation
  • Analyse avec QCacheGrind / WebGrind
  • Identifiez le goulot d'étranglement
  • Proposez et implémentez une optimisation
  • Comparez les performances avant/après

17.7 Groupe G : Symfony (Exercices 31-33)

Exercice 31 : Messenger handler asynchrone

Système de traitement d'images :

  • Message ProcessImage : imageId, transformations
  • Handler avec plusieurs transports (async, priority)
  • Middleware custom pour logging et timing
  • Retry avec backoff exponentiel
  • Test avec Transport::setup()

Exercice 32 : Serializer custom normalizer

Créez un normalizer pour Money :

  • Normalise : {"amount": 12.34, "currency": "EUR"}
  • Denormalise : retourne un objet Money
  • Support des groupes de sérialisation
  • Utilisez-le avec le Serializer Symfony

Exercice 33 : Security voter personnalisé

Voter ArticleVoter :

  • Attributes : VIEW, EDIT, DELETE, PUBLISH
  • Supporte les règles métier : author peut éditer, admin peut tout, moderator peut publier
  • Utilise AccessDecisionManager avec stratégie affirmative/unanimous
  • Testez avec AuthenticationTest

17.8 Groupe H : Livewire (Exercices 34-36)

Exercice 34 : Component CRUD avec validation

Livewire 3 component TaskList :

  • Propriétés : $tasks (Collection), $newTaskTitle
  • Méthodes : addTask(), toggleTask($id), deleteTask($id)
  • Validation en temps réel : title required, min:3, max:255
  • Événements : task-added, task-toggled, task-deleted
  • Loading state avec loading wire:loading

Exercice 35 : File upload avec progression

Component FileUploader :

  • Upload avec validation : max:10MB, mimes:pdf,docx,jpg
  • Barre de progression avec Alpine.js $wire.upload
  • Preview : image (thumbnail), PDF (icône)
  • Drag and drop avec Alpine.js
  • Stockage S3 avec storeAs

Exercice 36 : Alpine.js intégration avancée

Component ShoppingCart :

  • Alpine.js gestion du panier côté client
  • Synchronisation avec Livewire $wire
  • Animation de transition (ajout/suppression)
  • Persistance localStorage + sync serveur
  • Utilisez x-data, x-show, x-transition, x-model

17.9 Groupe I : WordPress (Exercices 37-39)

Exercice 37 : Custom Gutenberg block

Créez un block Gutenberg team-members :

  • block.json avec supports, attributes, styles
  • Edit : InspectorControls pour ajouter/modifier des membres
  • Save : RenderCallback côté PHP avec rendu sécurisé
  • Attributs : name, role, photo, bio, socialLinks
  • Dynamic block avec render_callback

Exercice 38 : WP REST API endpoint

Endpoint WordPress REST API personnalisé :

  • Route : /wp-json/mp/v1/stats
  • Méthodes : GET, POST
  • Retourne : total users, total posts, recent comments
  • Permission : admin uniquement
  • Cache : 5 minutes avec transient

Exercice 39 : WP-CLI command

Commande WP-CLI mp generate :

  • wp mp generate posts --count=50
  • wp mp generate users --count=10
  • wp mp generate all (posts + users + terms)
  • Barre de progression
  • Gestion d'erreurs et rollback

17.10 Groupe J : Architecture (Exercice 40)

Exercice 40 : Refactoring Legacy vers Clean Architecture

Vous recevez un contrôleur Laravel legacy de 500 lignes :

class OrderController extends Controller
{
    public function store(Request $request)
    {
        // Validation dans le contrôleur
        // Logique métier dans le contrôleur
        // Envoi d'email dans le contrôleur
        // Pas de tests
    }
}

Refactorisez vers :

  • Action class : StoreOrderAction (single responsibility)
  • DTO : OrderData (Spatie Laravel Data)
  • Service : OrderService
  • Repository : OrderRepository (interface + implémentation)
  • Notification : OrderConfirmation (Laravel Notification)
  • Tests : Pest pour chaque couche
  • Validation : Form Request StoreOrderRequest

Critères :

  • SOLID principles respectés
  • Testable unitairement
  • Pas de régression fonctionnelle

17.11 Rendu et Évaluation

Format de rendu

  • Repository Git par exercice (ou dossier structuré)
  • composer.json et phpunit.xml inclus
  • Tests verts (green suite)
  • Documentation courte d'installation

Grille d'évaluation

CritèrePoints
Fonctionnalité correcte40%
Qualité du code (SOLID, PSR)25%
Tests (coverage > 80%)20%
Documentation10%
Performance5%

Durée recommandée par exercice

  • Débutants (⭐) : 10-15 min
  • Intermédiaires (⭐⭐) : 20-30 min
  • Avancés (⭐⭐⭐) : 45-60 min
  • Expert (⭐⭐⭐⭐⭐) : 3-4h