Quand le CRUD classique ne suffit plus : Découvrez CQRS et Event Sourcing
\n\nAvez-vous déjà vécu cette situation où votre application PHP grandit, grandit... et qu'un beau jour, un simple UserController se transforme en monstre de 2000 lignes ? Ça vous parle ? Les bugs se multiplient, les performances chutent, et ajouter une nouvelle fonctionnalité donne des sueurs froides à toute l'équipe. Félicitations, vous avez atteint le stade où il est temps d'adopter des patterns architecturaux sérieux.
Aujourd'hui, nous allons parler de deux outils extrêmement puissants : CQRS (Command Query Responsibility Segregation) et Event Sourcing. Ne vous laissez pas impressionner par ces termes complexes. En réalité, ce sont simplement des façons élégantes de dire : "Arrêtons de mélanger la lecture et l'écriture des données" et "Stockons non pas l'état actuel, mais l'historique des modifications".
\n\nCet article s'adresse aux développeurs PHP qui maîtrisent déjà les bases de la POO et souhaitent faire passer leurs projets au niveau supérieur. Vous apprendrez à appliquer CQRS et Event Sourcing en pratique, vous verrez du code réel et vous comprendrez quand ces patterns sont vraiment nécessaires, et quand il vaut mieux les éviter.
\n\nQu'est-ce que CQRS ? Séparez les commandes et les requêtes
\n\nImaginez que votre code est un restaurant. Normalement, le serveur (le contrôleur) prend la commande (requête d'écriture), court en cuisine (la base de données), prépare le plat (traite les données) et le sert immédiatement (renvoie la réponse). Tout va bien tant qu'il y a 10 clients. Mais quand 1000 invités arrivent, la cuisine commence à suffoquer.
\n\nCQRS (Command Query Responsibility Segregation) propose de séparer les responsabilités : certains cuisiniers (les commandes) ne font que préparer, d'autres serveurs (les requêtes) ne font que servir. Techniquement, cela signifie que vous utilisez des modèles différents pour la lecture et l'écriture des données.
\n\nImplémentation de base de CQRS en PHP
\n\nVoyons à quoi cela ressemble en code. Commençons par un exemple simple sans framework, pour bien comprendre le principe.
\n\n// Une commande est un DTO avec les données pour modifier l'état\nclass CreateUserCommand {\n public function __construct(\n public readonly string $email,\n public readonly string $name\n ) {}\n}\n\n// Le gestionnaire de commande – la logique métier se trouve ici\nclass CreateUserHandler {\n public function __construct(\n private UserRepository $repository,\n private EventDispatcher $dispatcher\n ) {}\n\n public function handle(CreateUserCommand $command): void {\n $user = new User($command->email, $command->name);\n $this->repository->save($user);\n $this->dispatcher->dispatch(new UserCreatedEvent($user->getId()));\n }\n}\n\n// Une requête est un DTO pour obtenir des données\nclass GetUserQuery {\n public function __construct(\n public readonly int $userId\n ) {}\n}\n\n// Le gestionnaire de requête – lecture seule, aucune modification\nclass GetUserHandler {\n public function __construct(\n private UserReadRepository $readRepository\n ) {}\n\n public function handle(GetUserQuery $query): ?UserDTO {\n return $this->readRepository->findById($query->userId);\n }\n}\n\nRemarquez que nous avons deux repositories différents. UserRepository est responsable de l'écriture (il peut utiliser une logique métier complexe, des validations), tandis que UserReadRepository est responsable de la lecture (il peut s'agir d'une simple requête SQL ou même d'une table dénormalisée).
Le bus de commandes (Command Bus)
\n\nDans les projets réels, les commandes et les requêtes sont envoyées via un bus – un répartiteur central qui trouve automatiquement le gestionnaire approprié.
\n\n// Implémentation simple d'un Command Bus\nclass SimpleCommandBus {\n public function __construct(\n private array $handlers = []\n ) {}\n\n public function registerHandler(string $commandClass, object $handler): void {\n $this->handlers[$commandClass] = $handler;\n }\n\n public function dispatch(object $command): void {\n $handler = $this->handlers[$co