6. Ports, Bus de messages et Pragmatisme : L’utilisation du message Bus et CQRS pour éviter le…
L’architecture hexagonale n’est pas une fin en soi. Multiplier les interfaces et les fichiers sans raison revient à payer le coût de…
6. Ports, Bus de messages et Pragmatisme : L’utilisation du message Bus et CQRS pour éviter le boilerplate
L’architecture hexagonale n’est pas une fin en soi. Multiplier les interfaces et les fichiers sans raison revient à payer le coût de l’abstraction sans en tirer les bénéfices. L’objectif est de protéger ce qui porte du risque, et de simplifier le reste.
1. Les deux extrêmes à éviter
Trop de couplage : tout passe par Doctrine, le contrôleur ou l’admin. C’est rapide au début, mais la logique technique remonte dans le métier et le code devient difficile à tester.
Trop d’abstraction : une interface, un DTO, un mapper et un service intermédiaire pour chaque opération. L’architecture est “propre” sur le papier, mais personne ne peut la faire vivre au quotidien.
Le bon niveau : protéger les frontières qui portent du risque métier, simplifier là où le risque est faible.
2. Le bus comme port applicatif léger
Dans une version stricte de l’hexagonal, chaque cas d’usage s’expose via une interface dédiée — ce qui multiplie rapidement les fichiers.
Dans ce projet, CommandBusInterface, QueryBusInterface et EventBusInterface jouent ce rôle de manière plus légère. Un adaptateur d'entrée dispatche une commande sur le bus sans connaître le handler qui va la traiter :
Processor → CommandBus → Handler → Domaine
// Le processor API Platform ne connaît pas UploadDepositRequestDocumentHandler.
// Il traduit la requête HTTP en intention : "joindre des pièces justificatives à ce dossier".
$this->commandBus->dispatch(new UploadDepositRequestDocumentCommand(
requestNumber: $uriVariables['requestNumber'],
agency: $context['agency'],
files: $data->files,
));
Les événements de domaine suivent le même principe : quand un dossier passe automatiquement en PENDING après le dernier document joint, le handler récupère l’événement enregistré par l’agrégat et le dispatche sur un bus dédié.
DepositRequest.attachDocument() → enregistre DepositRequestBecamePendingEvent
Handler → releaseEvents() → EventBus → EventHandler
// Dans le handler, après la persistance
foreach ($depositRequest->releaseEvents() as $event) {
$this->eventBus->publish($event);
}
Ce choix réduit le boilerplate et garde un point d’entrée applicatif explicite, sans créer une interface par cas d’usage.
3. Ce que le bus ne résout pas
Un bus ne remplace pas un bon modèle métier. Les mêmes erreurs restent possibles derrière Messenger :
- des handlers qui font trop de choses (responsabilité diffuse) ;
- des commandes qui transportent des objets Doctrine ou des entités mutées ;
- un domaine anémique : des entités qui ne contiennent que des getters/setters et aucune règle métier. Toute la logique vit dans les handlers. L’entité est une coquille vide — une simple structure de données avec un nom de classe.
// DOMAINE ANÉMIQUE — l'entité ne décide de rien
class DepositRequest {
public function setStatus(string $status): void { $this->status = $status; }
public function getStatus(): string { return $this->status; }
}
// HANDLER QUI PORTE TOUTE LA LOGIQUE — mauvais signe
public function __invoke(ApproveDepositRequestCommand $command): void
{
$depositRequest = $this->repository->find($command->id);
// Ces règles devraient être dans l'entité, pas ici
if ($depositRequest->getStatus() !== 'PROCESSING') {
throw new \Exception('Cannot approve.');
}
$depositRequest->setStatus('APPROVED'); // mutation directe, sans intention
$depositRequest->setProcessedAt(new \DateTimeImmutable());
$this->repository->save($depositRequest);
}
Le bus est un mécanisme de transport et de découplage. Il n’invente pas les bonnes frontières.
4. Validation technique vs invariant métier
Ces deux notions semblent proches mais ont des responsabilités distinctes.
Validation technique (frontière d’entrée)
Vérifier que la donnée est présente, au bon format, dans les limites attendues. Elle se place sur les DTO d’entrée, via les attributs Assert de Symfony :
final class UploadDocumentInput
{
#[Assert\NotBlank]
#[Assert\File(maxSize: '10M', mimeTypes: ['application/pdf'])]
public UploadedFile $file;
}
Si cette validation échoue, le cas d’usage n’est jamais lancé. C’est un filtre de frontière, pas une règle métier.
Invariant métier (domaine)
Vérifier qu’une action est autorisée selon l’état actuel du système. Cette logique dépend du contexte et de l’état de l’objet — elle appartient au domaine :
// On ne peut joindre une pièce justificative qu'à un dossier encore en état DRAFT
public function attachDocument(DepositRequestDocument $document): void
{
if (DepositRequestStatus::DRAFT !== $this->status) {
throw DepositRequestTransitionNotAllowedException::becauseStatusIs($this->status, DepositRequestTransition::ATTACH_DOCUMENT);
}
// ...
}
Un attribut Assert\NotBlank ne peut pas exprimer "ce dossier n'est plus en état DRAFT". C'est pourquoi ces deux niveaux doivent rester séparés.
5. CQRS comme conséquence, pas comme objectif
Dès qu’on distingue commandes, requêtes et événements, une organisation CQRS émerge naturellement :
- Commande → modifie l’état, protège les invariants :
Command → Handler → Agrégat → Repository. - Requête → lit un modèle adapté à l’affichage :
Query → Handler → SQL optimisé → DTO de sortie. - Événement → propage ce qui s’est produit :
DomainEvent → EventBus → EventHandler.
L’intérêt est concret. Imaginons qu’un écran de liste de dossiers ait besoin d’afficher : numéro de dossier, statut, nom de l’artisan, nombre de pièces jointes, indicateur de retard. Sans CQRS, on charge l’agrégat complet DepositRequest avec toutes ses collections, ses value objects, ses relations — pour en extraire 5 champs. Avec une requête dédiée :
final class GetUserDepositRequestHistoryHandler implements QueryHandlerInterface
{
public function __invoke(GetUserDepositRequestHistoryQuery $query): array
{
// Requête SQL directe, optimisée pour l'affichage — pas de chargement d'agrégat
return $this->historyRepository->findByTriggeredBy(
$query->userEmail,
$query->page,
$query->limit,
);
}
}
Le modèle de lecture n’a pas besoin de protéger les invariants — il n’a besoin que de répondre vite. Séparer les deux évite que des contraintes de lecture (pagination, jointures, sérialisation) ne contaminent le modèle d’écriture.
Conclusion
Le pragmatisme consiste à supprimer le code inutile sans supprimer les bonnes frontières. Le bus simplifie l’entrée dans la couche applicative sans imposer une interface par cas d’usage.
Concrètement, comment tout cela se range-t-il dans les dossiers ? La fiche 07 présente la structure et un exemple complet.
메타데이터
- post_id
- ca2a3ec1a7a9
- slug
- 6-ports-bus-de-messages-et-pragmatisme-lutilisation-du-message-bus-et-cqrs-pour-éviter-le-ca2a3ec1a7a9
- url
- https://medium.com/@ahmedbhs/6-ports-bus-de-messages-et-pragmatisme-lutilisation-du-message-bus-et-cqrs-pour-%C3%A9viter-le-ca2a3ec1a7a9
- canonical_url
- https://medium.com/@ahmedbhs/6-ports-bus-de-messages-et-pragmatisme-lutilisation-du-message-bus-et-cqrs-pour-%C3%A9viter-le-ca2a3ec1a7a9
- author_url
- https://medium.com/@ahmedbhs
- status
- ok
- fetched_at
- 2026-06-17 08:20:12