Introduzione a CQRS in PHP puro: separa comandi e letture senza framework
Senti parlare di CQRS ovunque ma ti chiedi come applicarlo senza tirare fuori Symfony o Laravel? Buone notizie: possiamo esplorare la Command Query Responsibility Segregation in PHP puro con una semplice organizzazione e poche classi ben pensate. Apri l’editor, gettiamo insieme le fondamenta.
Prerequisiti: PHP 8.4 e configurazione minima
- PHP 8.4 installato (CLI o server web). Verifica la tua versione con
php -vper sfruttare le proprietàreadonly, i tipi strict e il supporto nativo ai first-class callables. - Composer (opzionale ma comodo per l’autoloading) e un server web leggero (
php -S 127.0.0.1:8000 -t publicè sufficiente per fare prove).
Se devi restare su una versione precedente, assicurati che le funzionalità utilizzate (promozione del costruttore, proprietà readonly, ecc.) siano disponibili oppure adatta gli snippet.
1. CQRS in due minuti
CQRS significa separare la lettura (Query) dalla scrittura (Command). Ottieni così due percorsi ottimizzati:
- I comandi modificano lo stato (creare un account, pubblicare un articolo) e non restituiscono dati applicativi.
- Le query leggono lo stato (elenco degli articoli, dettaglio di un account) senza effetti collaterali.
Questa separazione nasce dalla filosofia del DDD, ma puoi adottarla senza eccessivo formalismo. Benefici immediati:
- Test più semplici: testi i handler in maniera unitaria.
- Codice più leggibile: ogni handler fa una sola cosa.
- Scalabilità progressiva: puoi far evolvere il lato lettura in modo indipendente dal lato scrittura.
Ricorda però che CQRS introduce un po’ di complessità strutturale. Su un semplice CRUD potrebbe essere eccessivo. Usalo quando le regole di business o le esigenze di scalabilità lo richiedono.
2. Struttura di cartelle minimale in PHP nativo
Possiamo restare leggeri con un albero molto semplice:
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
Separiamo chiaramente Domain (regole di business), Application (casi d’uso) e Infrastructure (implementazioni concrete).
3. Modello di dominio minimalista
Creiamo un aggregato Article e il suo repository:
php
php
4. Lato Command: creare un articolo
Il comando trasporta l’intenzione. L’handler orchestra la logica di business.
php
php
Nota importante: l’handler non restituisce nulla. Se vuoi leggere di nuovo l’articolo, passerai da una Query.
5. Lato Query: elencare gli articoli
php
php
Esporre un array già pronto per la serializzazione evita di legare la presentazione al modello di dominio.
6. Un repository in memoria per partire
php
Potrai sostituire questa implementazione con Doctrine, PDO o un’API esterna in seguito senza toccare gli handler.
7. Mettere in piedi bus ultraleggeri
Il pattern CQRS diventa piacevole quando centralizzi la risoluzione degli handler:
php
php
Questi bus restano basilari ma bastano per comprendere la meccanica. In un progetto reale potresti collegarli a un contenitore di dependency injection (PHP-DI, Symfony DI, ecc.).
8. Esempio di bootstrap in public/index.php
php
L’operatore (...) (first-class callable), disponibile da PHP 8.1, è pienamente supportato in PHP 8.4. Se devi supportare una versione precedente (8.0 o inferiore), sostituiscilo con fn ($command) => $createHandler($command).
9. Progredire passo dopo passo
Una volta gettate le basi, puoi arricchire la soluzione:
- Validazione: aggiungi Value Object (Titolo, Contenuto) o una validazione con Symfony Validator prima di creare l’aggregato.
- Persistenza reale: implementa un repository MySQL (PDO) o PostgreSQL senza toccare gli handler.
- Eventi di dominio: pubblica un
ArticlePublishedquando il comando va a buon fine e consumalo in un altro livello. - Proiezione dedicata: crea una tabella ottimizzata per la lettura per query esigenti (paginazione, ricerca full-text).
- Test: scrivi test unitari per ogni handler e test funzionali per i bus.
10. Checklist per la prima messa in produzione
- Ogni comando ha un handler dedicato che non restituisce nulla.
- Ogni query è isolata e formatta i dati per il front-end.
- Le dipendenze tecniche vengono iniettate dall’esterno.
- Gli errori di business sono gestiti nel dominio (eccezioni specializzate, Value Object, ecc.).
- I test coprono comandi, query e bus.
Con questo scheletro puoi introdurre CQRS gradualmente in un progetto esistente o avviare un nuovo servizio senza framework. La chiave è mantenere chiaro il confine tra lettura e scrittura e far evolvere l’architettura per iterazioni. Buona sperimentazione!