Table des matières
- Comment utiliser simplement Messenger de Symfony pour respecter le pattern CQRS ?
- Les Commandes
- Query
- Installation du composant Messenger de Symfony
Comment utiliser simplement Messenger de Symfony pour respecter le pattern CQRS ?
En temps normal, quand je développe une application Symfony, je segmente mon code en 3 couches:
- Controller: La où je récupère la requête et où je transforme l'intention en une requête métier
- Manager: La où je lis les données, où j'effectue des actions métier et où je persiste les changements
- Repository: La où je communique avec le système de données (base de données MySQL par exemple)
Toutes les opérations de lecture et d'écriture se retrouvent dans les Controller/Manager/Repository associés.
Mais une autre segmentation, qui se nomme CQRS, est très utilisée en association avec l'event sourcing pour séparer les intentions de lecture et d'écriture.
Cela peut, par exemple, permettre d'allouer plus de serveurs à la lecture qu'à l'ecriture, ou inversement.
Les Commandes
Les commandes représentent donc les intentions d'écriture dans le système. Chaque commande devra implémenter l'interface Command
et définir
des attributs comme le ferai un modèle pour stocker les informations de la demande.
Par exemple, une commande pour bloquer un compte utilisateur pourrait avoir un champ userId
contenant l'id de l'utilisateur à bloquer.
<?php
declare(strict_types=1);
namespace App\Common\Command;
interface Command
{
}
Pour chaque commande, je crée un Handler
qui est chargé d'effectuer une action dans le système. Pour reprendre mon exemple de la commande
qui bloquerait un utilisateur, l'Handler de cette commande pourrait éventuelement avoir un UserRepository
en dépendance et faire un appel
à une fonction updateUser
de ce dernier.
<?php
declare(strict_types=1);
namespace App\Common\Command;
interface CommandHandler
{
}
Et enfin, le pattern CQRS recommande de créer un Bus de commande pour pouvoir facilement y intégrer des middleware par la suite.
Un middleware possible sur le Bus des commandes serait un middleware transactionel pour une base de données relationelle.
<?php
declare(strict_types=1);
namespace App\Common\Command;
interface CommandBus
{
function dispatch(Command $command): void;
}
Query
Les Query représentent, elles, des intentions de lecture. Chaque Query devra implémenter l'interface Query
et définir des attributs
qui représenteront les données utiles pour l'élaboration de la Query.
Par exemple, si j'imagine une Query qui retourne une liste paginée d'utilisateurs, deux attributs possibles seront: pageNumber
et pageSize
.
<?php
declare(strict_types=1);
namespace App\Common\Query;
interface Query
{
}
Comme pour les commandes, chaque Query a son propore Handler qui récupère l'intension de lecture et qui effectue la collecte des données.
<?php
declare(strict_types=1);
namespace App\Common\Query;
interface QueryHandler
{
}
En revanche, le QueryBus
, contrairement au CommandBus
, retourne un mixed
qui sera la réponse pour le Controller.
<?php
declare(strict_types=1);
namespace App\Common\Query;
interface QueryBus
{
function ask(Query $query): mixed;
}
Installation du composant Messenger de Symfony
Maintenant que les interfaces sont définies, je vais passer à l'implémentation. Le composant Symfony Messenger sera parfait pour mes implémentations de Bus.
Je vais commencer par l'installer:
composer require symfony/messenger
Et modifier la configuration de ce dernier comme ceci:
framework:
messenger:
default_bus: command.bus
buses:
command.bus:
middleware:
- validation
# - doctrine_transaction
query.bus:
middleware:
- validation
# event.bus:
# default_middleware: allow_no_handlers
# middleware:
# - validation
Et enfin, je vais faire deux classes qui vont venir wrapper le MessengerBusInterface
pour ne pas avoir à l'utiliser directement.
<?php
declare(strict_types=1);
namespace App\Common\Command;
use Symfony\Component\Messenger\MessageBusInterface;
final class MessengerCommandBus implements CommandBus
{
/**
* @var MessageBusInterface
*/
private $commandBus;
public function __construct(MessageBusInterface $commandBus)
{
$this->commandBus = $commandBus;
}
public function dispatch(Command $command): void
{
$this->commandBus->dispatch($command);
}
}
Pour le MessengerQueryBus
, je peux rajouter le trait HandleTrait
, qui simplifie la récupération de la réponse du Bus.
<?php
declare(strict_types=1);
namespace App\Common\Query;
use Symfony\Component\Messenger\HandleTrait;
use Symfony\Component\Messenger\MessageBusInterface;
final class MessengerQueryBus implements QueryBus
{
use HandleTrait {
handle as handleQuery;
}
public function __construct(MessageBusInterface $queryBus)
{
$this->messageBus = $queryBus;
}
public function ask(Query $query): mixed
{
return $this->handleQuery($query);
}
}
Et voilà, il ne reste plus qu'à créer des commandes et des queries pour satisfaire notre métier.