Symfony, Hexagonal architecture and CQRS

Software-Architecture Symfony

Symfony, Hexagonal architecture and CQRS

Publié le 20 janv. 2025

Image par @alexkixa

Dans cet article, je vais expliquer comment j’ai organisé mes derniers projets Symfony. J’utilise principalement l’architecture hexagonale et CQRS. Gardez à l’esprit que je ne vise pas à appliquer ces architectures de manière stricte. Je me contente de reprendre les concepts qui m’aident à créer une base de code simple et bien organisée.

Regardons la racine du projet : il n’y a rien de particulièrement inhabituel. J’ai conservé tous les dossiers et fichiers générés lors de l’installation de Symfony.

tree . -L 1                       
├── bin
├── composer.json
├── composer.lock
├── config
├── features
├── public
├── src
├── symfony.lock
├── tests
├── translations
├── var
└── vendor

Dans les sections suivantes, nous découvrirons comment j’ai organisé les sources de l’application en utilisant l’architecture hexagonale et comment CQRS m’a aidé à simplifier la modélisation des cas d’utilisation de lecture et d’écriture.

Mon approche de l’architecture hexagonale

The hexagonal architecture, or ports and adapters architecture, is an architectural pattern used in software design. It aims at creating loosely coupled application components that can be easily connected to their software environment by means of ports and adapters. This makes components exchangeable at any level and facilitates test automation.

https://en.wikipedia.org/wiki/Hexagonal_architecture_%28software%29

Le principal avantage de l’architecture hexagonale est qu’elle découple le cœur de votre application des entrées/sorties .

J’appelle le cœur de l’application le Domaine. C’est la partie de l’application où se trouve tout le code qui représente le problème que nous résolvons. Cette partie doit être sans effet de bord et ne doit pas dépendre d’outils, de frameworks ou de n’importe quelles technologies.

Les Sorties font référence aux outils dont l’application a besoin pour fonctionner, comme les appels réseau, les requêtes à une base de données, les opérations sur le système de fichiers, les “actual timestamps” ou la gestion de choses aléatoires. Toutes les Sorties se trouvent dans l’infrastructure. Les Entrées font référence à la manière dont le domaine est exposé au monde extérieur, par exemple via un contrôleur web ou une commande CLI. Elles se trouvent dans UserInterface.

Remarque : Consultez mon article de blog sur l’architecture hexagonale pour approfondir le sujet :

Sur cette base, ma première décision a été de diviser le répertoire src en trois zones : Domain, Infrastructure et UserInterface.

tree src/Domain/ -L 1
api/src/Domain/
├── Domain
├── Infrastructure
└── UserInterface

Règles de couplage:

  • Domain ne doit pas dépendre de Infrastructure ni de UserInterface.
  • Infrastructure et ‘UserInterface peuvent dépendre de Domain.

Je ne suis pas un grand fan de l’architecture en oignon, car je préfère garder mes projets aussi simples que possible. Avoir de nombreuses couches peut rendre la maintenance difficile, car cela nécessite de mettre toute l’équipe d’accord sur les règles de couplage et d’organisation. Même s’accorder avec soi-même peut être difficile, donc obtenir un consensus à plusieurs est souvent beaucoup plus complexe. Ici, nous suivons une règle simple : Domain ne doit pas utiliser d’entrées/sorties.

Parfois, j’ai dû créer des librairies parce que je ne trouvais aucune librairie open-source répondant à mes attentes. Pour éviter de coder directement dans le répertoire vendor, j’ai introduit une troisième zone appelée Librairies (cette zone est optionnelle). Ces librairies peuvent être utilisées à la fois dans les couches Domain, UserInterface et Infrastructure, mais leur utilisation ne doit pas enfreindre les règles de couplage définies pour ces zones.

tree src/Domain/ -L 1
api/src/Domain/
├── Domain
├── Infrastructure
├── Librairies
└── UserInterface

Règles de couplage : Librairies ne doit pas dépendre de Domain, UserInterface ou de Infrastructure.

Enfin, j’ai créé une sous-zone appelée Application dans la couche Infrastructure. Elle contient tout le code nécessaire pour que l’application soit opérationnelle, comme le code du framework (le noyau de Symfony et les personnalisations du framework), les data fixtures et les migrations. Dans l’exemple suivant, les dossiers Exception et Security contiennent des personnalisations du framework.

tree src/Infrastructure/Application -L 1 
api/src/Infrastructure/Application
├── Exception 
├── Fixture
├── Kernel.php
├── Migrations
├── Security
└── Kernel

Remarque :: Avec le recul, je ne conserverais pas ce dossier dans Infrastructure. Tout code lié à la personnalisation du Framework devrait aller dans un dossier dédié, appelé framework, situé dans le dossier Librairies, tandis que Fixtures et Migrations peuvent rester à la racine du dossier Infrastructure.

Se focaliser sur le métier

Un aspect très important pour moi est d’organiser la base de code autour des concepts métiers. J’évite de nommer des dossiers et des classes en fonction de patterns techniques comme Entity, ValueObject ou Repository, et surtout pas Provider, DataMapper ou Form. Les personnes non techniques doivent pouvoir comprendre l’objectif d’une classe simplement à partir de son nom.

Domain

tree src/Domain -L 1
api/src/Domain
├── Cartographer
└── Map

Puisque j’ai évité d’utiliser des termes techniques pour nommer les dossiers, il est facile d’imaginer que le projet concerne la création de cartes. Maintenant, jetons un œil à l’intérieur de Map :

tree src/Domain/Map -L 1
├── CartographersAllowedToEditMap.php   // ValueObject
├── Description.php                     // ValueObject
├── MapCreated.php                      // Event
├── MapId.php                           // ValueObject
├── MapName.php                         // ValueObject
├── Map.php                             // Root Aggregate
├── Maps.php                            // Repository Interface
├── Marker                              // All classes to design Marker entity
├── MarkerAddedToMap.php                // Event
└── UseCase                             // Use cases orchestration

Dans ce dossier, nous avons tout le code nécessaire pour concevoir l’agrégat Map. Comme vous pouvez le voir, je ne l’ai pas organisé par design patterns comme ValueObject, `Entity, ou autre.

Comme vous l’avez peut-être remarqué, l’entité Map a une relation one-to-many avec l’entité Marker. Toutes les classes nécessaires pour modéliser cette entité se trouvent dans le dossier Marker, qui est organisé de la même manière que le répertoire Map.

Le dossier UseCase contient tout le code nécessaire pour orchestrer les cas d’utilisation, tels que les commandes, leurs handlers et les validations métier.

*Astuce : Je n’ajoute pas le suffixe “Repository” aux dépôts. À la place, j’essaie d’utiliser un concept métier pour le nom, comme ProductCatalog pour un agrégat Product. Si je ne trouve pas de concept métier adapté, j’utilise la forme plurielle du nom de l’agrégat, car un repository représente une collection d’objets.

J’organise les racines des dossiers Infrastructure et UserInterface de la même manière que pour le Domain.

Infrastructure

tree src/Infrastructure -L 1            
api/src/Infrastructure
├── …
├── Cartographer
└── Map
        └── InMemoryMaps.php
        └── PostgreSqlMaps.php

UserInterface

tree src/InterfaceUtilisateur -L 1
api/src/InterfaceUtilisateur
├── …
├── Cartographe
└── Carte
    ├── WebAjouterMarqueur.php
    ├── CliAjouterMarqueur.php

Mon approche de CQRS

Starting with Command Query Responsibility Segregation, CQRS is simply the creation of two objects where there was previously only one. The separation occurs based upon whether the methods are a command or a query (the same definition that is used by Meyer in Command and Query Separation, a command is any method that mutates state and a query is any method that returns a value).

Greg Young

L’idée principale du CQRS est la séparation des côtés lecture et écriture. Vous pouvez utiliser des modèles différents pour écrire (commandes) et lire (requêtes). J’apprécie le concept d’avoir deux modèles petits et simples, dédiés à des objectifs spécifiques : lecture ou écriture, plutôt que de s’appuyer sur un seul gros modèle.

De plus, lorsque vous liez des agrégats par leurs identifiants au lieu de références, les cas d’utilisation complexes de lecture peuvent devenir difficiles. Comment récupérer des informations provenant de plusieurs agrégats ? Il est plus simple d’interroger directement la base de données plutôt que de fusionner les données de plusieurs agrégats.

Remarque : Consultez mon article de blog pour comprendre la différence entre CQS et CQRS :

Pour gérer cela, j’ai décidé de diviser chaque sous-dossier du domaine en deux zones : Command et Query.

tree src/Domain/ -L 2
api/src/Domain/
├── Cartographer
│   ├── Command
│   └── Query
└── Map
    ├── Command
    └── Query

Règles de couplage Command ne doit pas dépendre de Query et vice-versa.

Attention : Utiliser le CQRS, tel que défini par Greg Young, ne signifie pas introduire une complexité inutile dans votre application. Vous n’avez pas besoin d’un bus de commandes et de requêtes, d’une architecture basée sur l’event sourcing ou de multiples bases de données pour l’appliquer. J’ai choisi de séparer les cas d’utilisation d’écriture et de lecture parce que cela rendait ma base de code plus simple et plus claire.

Conclusion

Après des années à chercher l’architecture parfaite, j’ai réalisé qu’elle n’existe pas. Je préfère utiliser des concepts architecturaux qui rendent mon travail quotidien et celui de mes coéquipiers plus agréable. Cette organisation a été appliquée à plusieurs projets en production. L’un d’entre eux est un projet personnel , conçu pour créer des cartes sans dépendre de Google Maps. Les autres sont des projets professionnels utilisés quotidiennement par des utilisateurs.

Recevez chaque mois des conseils tech pratiques

Abonnez-vous pour recevoir une newsletter pleine de retours d’expérience, d’astuces et de bonnes pratiques. Restez en avance dans le jeu du code !

Vos données resteront strictement confidentielles et seront exclusivement utilisées pour communiquer avec vous. Soyez assuré(e) qu'elles ne seront jamais vendues à des tiers.