Initiation à CQRS en PHP vanille : séparer commandes et lectures sans framework
Tu entends parler de CQRS partout mais tu te demandes comment l'appliquer sans dégainer Symfony ou Laravel ? Bonne nouvelle : on peut explorer la Command Query Responsibility Segregation en PHP vanille avec juste une organisation claire et quelques classes bien pensées. Prends ton éditeur, on pose les bases ensemble.
Pré-requis : PHP 8.4 et configuration minimale
- PHP 8.4 installé (CLI ou serveur web). Vérifie ta version avec
php -vpour profiter des propriétésreadonly, des types stricts et du support natif des first-class callables. - Composer (optionnel mais pratique pour l'autoloading) et un serveur web léger (
php -S 127.0.0.1:8000 -t publicsuffit pour expérimenter).
Si tu dois rester sur une version antérieure, assure-toi que les features utilisées (constructeurs promotionnés, propriétés readonly, etc.) sont disponibles ou adapte les snippets.
1. CQRS en deux minutes chrono
CQRS consiste à séparer la lecture (Query) de l'écriture (Command). On obtient deux chemins optimisés :
- Les commandes mutent l'état (créer un compte, publier un article) et ne renvoient pas de données applicatives.
- Les requêtes lisent l'état (liste des articles, détail d'un compte) sans effet de bord.
Cette séparation découle naturellement de la philosophie DDD, mais tu peux l'adopter sans tout le formalisme. Les gains immédiats :
- Tests plus simples : tu testes des handlers unitaires.
- Code plus lisible : chaque handler fait une seule chose.
- Scalabilité progressive : tu peux faire évoluer le côté lecture indépendamment du côté écriture.
Rappelle-toi toutefois que CQRS ajoute un peu de complexité structurelle. Sur un CRUD basique, c'est peut-être trop lourd. Utilise-le quand les règles métiers ou les besoins de scalabilité le justifient.
2. Structure de dossier minimale en PHP natif
On peut rester léger avec une arborescence simple :
src/
Domain/
Article.php
ArticleRepository.php
Application/
Command/
CreateArticle/
CreateArticleCommand.php
CreateArticleHandler.php
Query/
ListArticles/
ListArticlesQuery.php
ListArticlesHandler.php
Bus/
CommandBus.php
QueryBus.php
Infrastructure/
InMemoryArticleRepository.php
public/
index.php
On distingue Domain (règles métier), Application (cas d’usage) et Infrastructure (implémentations concrètes).
3. Modèle métier minimaliste
Créons un agrégat Article et son repository :
php
php
4. Côté Command : créer un article
La commande transporte l'intention. Le handler orchestre la logique métier.
php
php
Remarque importante : le handler ne retourne rien. Si tu veux relire l’article, tu passeras par une Query.
5. Côté Query : lister les articles
php
php
On expose ici un tableau prêt à être sérialisé, sans dépendre du modèle domaine dans la couche présentation.
6. Un repository mémoire pour démarrer
php
Tu pourras remplacer cette implémentation par une version Doctrine, PDO ou API externe plus tard, sans toucher aux handlers.
7. Mettre en place des bus ultra-légers
Le pattern CQRS devient agréable quand tu centralises la résolution des handlers :
php
php
Ces bus restent naïfs mais suffisent pour comprendre la mécanique. Dans un vrai projet, tu pourrais les brancher sur un conteneur d’injection de dépendances (PHP-DI, Symfony DI, etc.).
8. Exemple d’amorçage dans public/index.php
php
L’opérateur (...) (first-class callable) disponible depuis PHP 8.1 est pleinement pris en charge par PHP 8.4. Si tu dois supporter une version antérieure (8.0 ou moins), remplace-le par fn ($command) => $createHandler($command).
9. Aller plus loin progressivement
Une fois cette base en place, tu peux enrichir ta solution :
- Validation : ajoute des Value Objects (Titre, Contenu) ou une validation Symfony Validator avant de créer ton agrégat.
- Persistance réelle : implémente un repository MySQL (PDO) ou PostgreSQL sans toucher aux handlers.
- Événements de domaine : publie un
ArticlePublishedquand la commande réussit, puis consomme-le dans une autre couche. - Projection dédiée : crée une table optimisée lecture pour les queries gourmandes (pagination, recherche full-text).
- Tests : écris des tests unitaires pour chaque handler et des tests fonctionnels pour les bus.
10. Checklist pour ta première mise en production
- Chaque commande a un handler dédié qui ne retourne rien.
- Chaque query est isolée et formate les données pour le front.
- Les dépendances techniques sont injectées depuis l’extérieur.
- Les erreurs métiers sont gérées dans le domaine (exceptions spécialisées, Value Objects, etc.).
- Les tests couvrent commandes, queries et bus.
Avec ce squelette, tu peux introduire CQRS progressivement dans un projet existant ou démarrer un nouveau service sans framework. La clé reste de garder la frontière lecture/écriture claire et de faire évoluer ton architecture par itérations. Bonne expérimentation !