Événements de domaine avec Symfony Messenger : sortir les effets de bord de vos agrégats
Un agrégat qui envoie un email quand une commande est expédiée connaît le SMTP. C'est le mauvais type de connaissance pour une classe dont le seul travail est de protéger un invariant métier.
Mauvais - l'agrégat va chercher dans l'infrastructure
final class Order
{
public function ship(Mailer $mailer): void
{
$this->status = OrderStatus::SHIPPED;
$mailer->send(new ShippingConfirmationEmail($this->customerEmail));
}
}
Order dépend maintenant de Mailer, qui dépend d'un transport, qui
dépend d'une configuration. Tester ship() veut dire mocker un service
d'email pour vérifier une transition d'état. Et chaque nouvel effet de
bord - une notification Slack, une mise à jour de points de fidélité -
veut dire élargir encore le constructeur de l'agrégat.
Bon - l'agrégat enregistre, Messenger distribue
final class Order
{
private array $events = [];
public function ship(): void
{
$this->status = OrderStatus::SHIPPED;
$this->events[] = new OrderWasShipped($this->id);
}
public function releaseEvents(): array
{
$events = $this->events;
$this->events = [];
return $events;
}
}
final class ShipOrderHandler
{
public function __construct(
private readonly OrderRepository $orders,
private readonly MessageBusInterface $eventBus,
) {}
public function __invoke(ShipOrder $command): void
{
$order = $this->orders->getById($command->orderId);
$order->ship();
$this->orders->save($order);
foreach ($order->releaseEvents() as $event) {
$this->eventBus->dispatch($event);
}
}
}
OrderWasShipped est un objet simple sans dépendance au framework.
Symfony Messenger le distribue à autant de handlers que nécessaire - un
mailer, une notification Slack, un projecteur de modèle de lecture - sans
que Order sache qu'ils existent. Tester ship() devient une assertion
sur releaseEvents(), plus un mock.
Le compromis : une classe d'événement et un handler par effet de bord, là où un appel de méthode direct suffisait en une ligne. Ça vaut le coup dès qu'un agrégat a plus d'une raison de notifier l'extérieur, ou dès que ces effets de bord doivent survivre à une nouvelle tentative sans rejouer le changement d'état lui-même.