Implementare un Command Bus con Laravel-Tactician

Scopriamo Laravel-Tactician, un ottimo package che fa da wrapper per Tactician, della PHP League, su Laravel!
francesco
Francesco Malatesta
27/04/2016 in Package

Mai sentito parlare di Tactician?

Si tratta di uno dei package della PHP League, un Command Bus semplice e flessibile. Un ottimo strumento per organizzare al meglio il codice della propria applicazione (quando necessario) che in alcuni frangenti può evitare tanta, tanta duplicazione inutile. E a noi, che il codice cerchiamo di scriverlo bene, la duplicazione proprio non ci piace.

In questo articolo daremo uno sguardo al package laravel-tactician, di jildertmiedema, che fa da wrapper di Tactician su Laravel, facilitandone l'uso e l'adozione.

Command Bus?

Nota: se sai già cos'è un Command Bus, salta pure alla parte successiva.

Come lo stesso sito di Tactician spiega, un Command Bus viene implementato combinando il Command Pattern con un service layer. Lo scopo di un Command Bus è prendere determinati oggetti, i Command, e "gestirli" facendoli corrispondere ad un altro tipo di oggetto, un Handler ad essi collegati.

Il Command in se può essere visto come un semplice DTO, che mantiene dei dati che, successivamente, verranno usati dall'Handler che effettua l'azione di cui è responsabile.

Certo, la domanda sorge spontanea: perché tutto questo?

Sicuramente per una migliore organizzazione del codice. Facciamo un caso pratico, per capirci meglio.

Supponiamo di scrivere un blog. In gioco ci sono tre entità che lavorano insieme.

  • l'articolo, che non ha bisogno di presentazioni;
  • l'autore dell'articolo;
  • le categorie associate all'articolo, una o più di una;

Proviamo ad immaginare la procedura di salvataggio di un nuovo articolo.

  1. viene creato un nuovo oggetto Articolo;
  2. a questo articolo viene associato l'Autore;
  3. anche le categorie vengono associate all'articolo;
  4. l'articolo viene finalmente salvato su database;

Ben quattro step differenti, che tuttavia rappresentano un flusso preciso e definito.

La cosa più logica da fare è una sola: scrivere il codice in un controller, salvare tutto e via. Facciamo un test, ci assicuriamo che funzioni. Poi a casa, la giornata è andata.

Il giorno dopo arriva il capo e dice: "dobbiamo fare in modo che il nostro blog esponga delle API con cui potersi interfacciare".

Anche qui, nessun problema. Creiamo un nuovo controller ad-hoc, esponiamo le route in modo opportuno, copiamo ed incolliamo il vecchio codice e via. Adesso funziona tutto sia da una parte che dall'altra.

NO! FERMI TUTTI!

Abbiamo palesemente duplicato del codice.

Ed è qui che entra in gioco il Command Bus. Al posto di scrivere la stessa cosa due volte, possiamo:

  • creare una classe SaveArticleCommand, in cui specificare quale articolo bisogna salvare, quale autore e quali categorie associare ad esso;
  • creare una classe SaveArticleCommandHandler, in cui specificare la procedura di salvataggio;
  • usare un Command Bus che, automaticamente, mappi SaveArticleCommand con SaveArticleCommandHandler, eseguendo il codice presente nel secondo quando richiamiamo il primo;

Bingo! Adesso, sia dal controller "normale" che da quello delle API, basterà richiamare il Command Bus e basta. Tempo risparmiato, codice non più duplicato e la consapevolezza di aver fatto qualcosa di buono per il mondo.

Adesso che sappiamo, per sommi capi, cos'è un Command Bus, vediamo a cosa serve il package oggetto di questo articolo.

Il Package

Possiamo installare Laravel-Tactician usando Composer, come al solito.

Da linea di comando, eseguiamo

composer require jildertmiedema/laravel-tactician

e, una volta terminata la procedura di installazione, ricordiamoci di aggiungere

JildertMiedema\LaravelTactician\TacticianServiceProvider

all'elenco dei service provider presenti in config/app.php.

A questo punto, eseguiamo

php artisan vendor:publish

per copiare il file di configurazione del package in config/tactician.php.

In questo file potremo impostare i namespace "di base" per i Command e per gli Handler.

Per capirci, supponiamo di avere le due classi viste prima:

  • SaveArticleCommand;
  • SaveArticleCommandHandler;

Nel file di configurazione, di default troveremo

<?php

return [
    'commandNamespace' => 'App\Commands',
    'handlerNamespace' => 'App\Handlers\Commands',
];

Ciò significa che, quando chiederemo al nostro Command Bus di eseguire il command SaveArticleCommand, questo verrà cercato nel namespace App\Commands.

Il relativo Handler, dallo stesso nome del command con l'aggiunta di Handler alla fine, verrà cercato in App\Handler\Commands.

Tutto qui! Dovremo quindi impostare il commandNamespace e l'handlerNamespace in base al nostro progetto.

Uso del Command Bus

A questo punto siamo pronti ad usare il Command Bus. Partiamo facendo un esempio di Command:

<?php

namespace App\Commands;

use App\User;
use App\Article;
use App\Category;

class SaveArticleCommand {

    private $article;
    private $user;
    private $categories;
    
    public function __construct(Article $article, User $user, Categories $category) {
        $this->article = $article;
        $this->user = $user;
        $this->categories = $categories;
    }
    
    public function getArticle() {
        return $this-article;
    }
    
    public function getUser() {
        return $this-user;
    }
    
    public function getCategories() {
        return $this-categories;
    }
}

Ed il rispettivo esempio di Handler:

<?php

namespace App\Commands\Handlers;

use App\Commands\SaveArticleCommand;

class SaveArticleCommandHandler {
    
    public function handle(SaveArticleCommand $command) {
        $article = $command->getArticle();
        $user = $command->getUser();
        $categories = $command->getCategories();
        
        // associo l'articolo all'autore
        $article->user()->associate($user);
        
        // salvo l'articolo
        $article->save();
        
        // associo le categorie all'articolo
        $article->categories()->sync($categories->pluck('id'));
    }
    
}

A questo punto, l'ultimo step consiste nell'includere il comodo DispatchesCommands nel nostro controller:

...

class ArticleController extends BaseController {
    use DispatchesCommands;

...

Siamo pronti: tutto quello che serve è chiamare il metodo dispatch nel metodo del controller che ci interessa.

...

class ArticleController extends BaseController {
    use DispatchesCommands;

    public function postAdd() {
        ...
        
        $this-dispatch(new SaveArticleCommand(
                $article,
                $user,
                $categories
            )
        );
        
        ...
    }

...

Al resto ci pensa il nostro Command Bus!

Concludendo...

Sicuramente Laravel-Tactician può essere un'ottima scelta se si vuole migliorare il codice della propria applicazione, in un momento in cui la code base inizia a crescere, le necessità iniziano a diversificarsi ma si vuole mantenere comunque un certo livello di qualità.

Voi l'avete provato già? Fateci sapere cosa ne pensate.