Ivan Centamori

EN IT


Il pattern CQRS

Se hai mai lavorato su un'applicazione web nata piccola, ma che con il tempo cresce e diventa di medie o grandi dimensioni, conosci bene il problema della manutenibilità del codice. All'inizio tutto è pulito e ordinato, ma man mano che il progetto si espande, i Controller diventano enormi file da migliaia di righe e i Model si trasformano in God Objects (mostri onniscienti) che gestiscono di tutto: le relazioni complesse, la business logic di salvataggio, il calcolo delle statistiche, fino all'invio delle email o alle chiamate ad API esterne.

Immagina un classico UserController@store: riceve i dati, crea l'utente, gli assegna un ruolo, scrive un log nel database, iscrive l'utente alla newsletter su Mailchimp e infine spara un'email di benvenuto. Un incubo da leggere, mantenere e testare.

È qui che entra in gioco il pattern CQRS (Command Query Responsibility Segregation). Questo è un pattern architetturale concettualmente molto semplice e diretto: il principio di base è quello di separare le operazioni che leggono i dati da quelle che li modificano.

In un'architettura CRUD (Create, Read, Update, Delete) tradizionale, usiamo lo stesso modello Eloquent (es. User) sia per salvare un nuovo utente sia per recuperarne la lista. Il problema è che le esigenze di lettura e scrittura sono quasi sempre diverse. Per visualizzare una dashboard potresti aver bisogno di unire (JOIN) 5 tabelle diverse, mentre per aggiornare il nome dell'utente ti basta toccare un solo record.

Con il CQRS, abbandoniamo l'approccio "tutto in uno" e dividiamo il nostro sistema in due emisferi distinti:

Vantaggi e svantaggi

Introdurre il CQRS aggiunge un livello di complessità iniziale: ci troveremo a creare più file (classi) e directory rispetto a un approccio base. Tuttavia, in applicazioni enterprise o in forte crescita, i benefici a lungo termine saranno enormi:

  1. Ottimizzazione asimmetrica: In quasi tutte le applicazioni, le letture sono molto più frequenti delle scritture (spesso con un rapporto di 10:1 o 100:1). Pensa al catalogo prodotti di un e-commerce: migliaia di utenti visualizzano i prodotti (Lettura), ma l'amministratore ne modifica il prezzo o la giacenza solo raramente (Scrittura). Separando le responsabilità, puoi ottimizzare i due lati in modo indipendente. Potresti usare pesanti e sicure transazioni Eloquent per le scritture, e query SQL grezze o un database in memoria (come Redis o Elasticsearch) per avere letture fulminee.
  2. Controller snelli e responsabili: Dimentica il vecchio mantra "Fat Model, Skinny Controller". Con il CQRS passiamo a "Skinny Controller, Focused Handlers". I tuoi controller si ridurranno letteralmente a 3-4 righe di codice. Il loro unico scopo sarà ricevere la Request HTTP, validarla, e delegare il lavoro "sparando" un Command o una Query.
  3. Testabilità estrema: Testare un intero Controller significa dover simulare (mockare) richieste HTTP, middleware e routing. Nel CQRS, la logica risiede in classi PHP pure (gli Handler). In Pest o PHPUnit, ti basterà istanziare il tuo Command con dei dati fittizi, passarlo all'Handler e verificare l'output nel database. Semplice e isolato.
  4. Codice "parlante" (Screaming Architecture): Come teorizzato da Uncle Bob (Robert C. Martin), l'architettura del tuo software dovrebbe "urlare" il suo scopo. Leggere una directory piena di file come BanUserCommand, ApplyDiscountCodeCommand e GetTrendingArticlesQuery ti dice esattamente, a colpo d'occhio, cosa fa la tua applicazione dal punto di vista del business, senza dover aprire e decifrare le rotte o i metodi dei controller.

Implementare il CQRS in Laravel

Laravel non forza l'uso del CQRS, ma ci offre out-of-the-box tutti gli strumenti nativi che servono per implementarlo in modo elegante, come ad esempio il Command Bus (la facciata Bus).

1. La struttura delle progetto

Abbandoniamo per un attimo l'idea di buttare tutta la logica dentro app/Http/Controllers (che dovrebbero occuparsi solo del livello di trasporto HTTP) e creiamo un namespace CQRS dedicato alla nostra logica di business:

app/
├── CQRS/
│   ├── Commands/
│   │   └── Article/
│   │       ├── PublishArticleCommand.php
│   │       └── PublishArticleHandler.php
│   └── Queries/
│       └── Article/
│           ├── GetPublishedArticlesQuery.php
│           └── GetPublishedArticlesHandler.php

In questa struttura avremo due sottocartelle principali dedicate a Queries e Commands, che possiamo ancora dividere per ambito o per entità (come Article nel nostro caso).

Noterai che per ogni azione abbiamo due file distinti:

2. La scrittura: Commands e Handlers

Il nostro Command non è altro che un DTO (Data Transfer Object). Sfruttando le proprietà readonly introdotte nelle ultime versioni di PHP, possiamo creare un oggetto immutabile perfetto per trasportare i dati dal Controller all'Handler.

namespace App\CQRS\Commands\Article;

class PublishArticleCommand
{
    public function __construct(
        public readonly string $title,
        public readonly string $content,
        public readonly int $authorId
    ) {}
}

Il Command Handler, invece, è la classe operativa che conterrà la business logic. Il suo unico metodo pubblico sarà handle(), che riceverà il Command appena creato ed eseguirà l'azione sul database.

namespace App\CQRS\Commands\Article;

use App\Models\Article;
use Illuminate\Support\Str;

class PublishArticleHandler
{
    public function handle(PublishArticleCommand $command): Article
    {
        $article = Article::create([
            'title' => $command->title,
            'slug' => Str::slug($command->title),
            'content' => $command->content,
            'author_id' => $command->authorId,
            'published_at' => now(),
        ]);

        return $article;
    }
}

3. La lettura: Queries e Handlers

L'oggetto Query funziona esattamente come il Command: trasporta i parametri della nostra ricerca.

namespace App\CQRS\Queries\Article;

class GetPublishedArticlesQuery
{
    public function __construct(
        public readonly int $limit = 10,
        public readonly ?string $search = null
    ) {}
}

E il relativo Query Handler? Qui sta il vero superpotere del CQRS. Siccome stiamo solo leggendo, non siamo obbligati a istanziare pesanti modelli Eloquent. Se la nostra pagina ha bisogno di massima velocità, possiamo usare il Query Builder nativo di Laravel o addirittura interrogare una cache.

namespace App\CQRS\Queries\Article;

use Illuminate\Support\Facades\DB;
use Illuminate\Support\Collection;

class GetPublishedArticlesHandler
{
    public function handle(GetPublishedArticlesQuery $query): Collection
    {
        $dbQuery = DB::table('articles')
            ->select('id', 'title', 'slug', 'published_at')
            ->whereNotNull('published_at');

        if ($query->search) {
            $dbQuery->where('title', 'like', '%' . $query->search . '%');
        }

        return $dbQuery->orderByDesc('published_at')
                       ->limit($query->limit)
                       ->get();
    }
}

4. Il Command Bus

A questo punto abbiamo i pezzi del puzzle, ma come facciamo a dire a Laravel che quando spediamo un PublishArticleCommand deve eseguire proprio il PublishArticleHandler? Usiamo la facciata Bus.

Possiamo creare un Service Provider dedicato e mappiamo le nostre classi nel metodo boot:

namespace App\Providers;

use Illuminate\Support\ServiceProvider;
use Illuminate\Support\Facades\Bus;

class CqrsServiceProvider extends ServiceProvider
{
    public function boot(): void
    {
        Bus::map([
            PublishArticleCommand::class => PublishArticleHandler::class,
            GetPublishedArticlesQuery::class => GetPublishedArticlesHandler::class,
        ]);
    }
}

5. Il controller

Anziché creare un "mostro" in grado di validare, manipolare stringhe, salvare su database e lanciare eventi, ora il nostro controller fa solo due cose: riceve una richiesta validata e la smista.

namespace App\Http\Controllers;

use Illuminate\Support\Facades\Bus;
use App\Http\Requests\StoreArticleRequest;
use App\CQRS\Commands\Article\PublishArticleCommand;
use App\CQRS\Queries\Article\GetPublishedArticlesQuery;

class ArticleController extends Controller
{
    public function index()
    {
        $query = new GetPublishedArticlesQuery(
            limit: request('limit', 20),
            search: request('search')
        );

        $articles = Bus::dispatchSync($query);

        return response()->json($articles);
    }

    public function store(StoreArticleRequest $request)
    {
        $validated = $request->validated();

        $command = new PublishArticleCommand(
            $validated['title'],
            $validated['content'],
            auth()->id()
        );

        $article = Bus::dispatchSync($command);

        return response()->json($article, 201);
    }
}

Adottare il CQRS in Laravel cambia radicalmente il modo in cui pensi alla tua applicazione. La separazione degli intenti (leggere vs scrivere) ti forza a ragionare in termini di comportamenti ("Cosa vuole fare l'utente in questo momento?") invece che di semplici interazioni CRUD con un database.

Certo, non ha senso usare questa architettura per un semplice blog personale o per una To-Do list. Ma se stai lavorando su un progetto destinato a scalare, o se senti che i tuoi controller stanno sfuggendo di mano, prova a isolare l'azione più complessa del tuo sistema in un Command Handler. Scommetto che non tornerai più indietro!