Makina Blog

Le blog Makina-corpus

Itéra­tions vers le DDD et la clean archi­tec­ture avec Symfony (1/2)


Pourquoi et comment avons nous fait le choix de faire évoluer la concep­tion de nos projets Symfony, et quelles erreurs avons-nous faites ?

Sommaire

Cet article pose un regard de haut niveau sur les choix effec­tués dans deux projets que nous avons réali­sés qui s’orientent progres­si­ve­ment vers l’ar­chi­tec­ture hexa­go­nale, en détaillant sommai­re­ment les raisons de ces choix.

Il sera suivi par un second article détaillant les évolu­tions effec­tuées dans une seconde itéra­tion de l’évo­lu­tion de la concep­tion. Des conclu­sions et des pistes d’amé­lio­ra­tion de la solu­tion seront alors détaillées par la suite.

Note impor­tante : cet article n’a pas voca­tion à détailler l’ Archi­tec­ture Hexa­go­nale ni le Domain Driven Design, certaines parties pour­ront être diffi­ciles à appré­hen­der pour les néophytes quand bien même elles ne sont pas indis­pen­sables à la compré­hen­sion globale.

Un peu de contexte

En l’an de grâce 2018, et ce après un sérieux coup à la tête, nous avons décidé, de façon dérai­son­nable et pleins d’en­train d’ar­chi­tec­tu­rer un projet autour d’un bus de messages en s’ap­pro­chant de la métho­do­lo­gie CQS (Command Query Sepa­ra­tion).

Image
Oh !
Oh !

Pour faire court, le pattern CQS consiste à sépa­rer les lectures des écri­tures dans une appli­ca­tion en deux silos bien distincts. Nous avons dans le cadre de ce projet décidé d’orien­ter la couche métier, que nous nomme­rons le Domaine, en utili­sant la métho­do­lo­gie DDD (Domain Driven Design). Le but ici est de rendre le projet plus facile à main­te­nir dans le temps, et plus rési­lient aux bonds tech­no­lo­giques qui lui seront impo­sés lors de sa main­te­nance sur le long terme.

Pour bien comprendre les enjeux ici, on parle d’un site utilisé par un peu plus de 150 000 utili­sa­teurs ayant été actifs en 3 ans de produc­tion, et ayant soumis entre 40 000 et 80 000 demandes par an. Ce chiffre varie selon les années, mais va globa­le­ment en gran­dis­sant car de nouveaux types de demandes s’ajoutent chaque année. Chaque demande est ensuite instruite manuel­le­ment par un gestion­naire et parcourt un work­flow métier complexe, puis se termine par un ensemble de tâches auto­ma­ti­sées dont certaines donnant lieu à des envois d’ordre de paie­ment bancaire (le paie­ment en lui-même étant géré à son tour par un progi­ciel de gestion comp­table externe).

L’ap­pli­ca­tion est donc assujet­tie à un grand volume de lectures et d’écri­tures concur­rentes. Rien d’ex­tra­or­di­naire en appa­rence car nous n’avons en aucun cas des chiffres dépas­sant le million. Néan­moins, elle subit régu­liè­re­ment des pics de fréquen­ta­tion sur des périodes iden­ti­fiées, où le volume de demandes ouvertes en une jour­née peut monter à plusieurs milliers et ce durant un à deux mois constants, le plus géné­ra­le­ment sur une plage horaire restreinte à quelques heures à peine. Durant ces périodes de charge excep­tion­nelles, le site n’a pas le droit de faillir ; qui plus est, il arrive parfois que des types d’offres excep­tion­nelles soient mises à dispo­si­tion ponc­tuel­le­ment, ce qui crée d’au­tant plus de charge en dehors des périodes habi­tuelles, et impose d’avoir une infra­struc­ture dimen­sion­née pour soute­nir les pics tout au long de l’an­née.

Foule de gens
Foule de gens | CHUT­TERS­NAP via Unsplash

L’ap­pli­ca­tion dispose de deux inter­faces utili­sa­teur :

  • Un front-office, une vitrine acces­sible aux clients de notre client, qui leur permet de consul­ter les offres dispo­nibles, et de dépo­ser leurs demandes de dossier pour en béné­fi­cier.

  • Un back-office de gestion métier, qui sert à instruire les demandes, chacune d’entre elles ayant un work­flow de gestion complexe, parfois divergent, et soumis à de nombreuses vali­da­tions.

Reve­nons à notre concep­tion, en 2018, le site histo­rique utili­sait Drupal comme base tech­nique, il était alors une version très primi­tive du produit que nous devions construire. Le site d’ori­gine gérait un unique type de demande, contre une ving­taine aujour­d’hui. Il ne repré­sen­tait qu’une infime partie du volume supporté par l’ap­pli­ca­tion actuelle. Les autres types de demande étant à cette époque encore trai­tés manuel­le­ment sur papier par les gestion­naires.

Sachant que le nombre d’uti­li­sa­teurs ainsi que le volume de demandes annuelles atten­dus étaient alors incon­nus, l’idée était à ce moment là d’es­sayer de maxi­mi­ser l’ex­ten­si­bi­lité hori­zon­tale, de conce­voir une appli­ca­tion scalable :

  • Conser­ver la possi­bi­lité, à tout moment, de rempla­cer les lectures sur la base de données par un Read Model si néces­saire, sans avoir à chan­ger le code de l’in­ter­face utili­sa­teur ou du modèle métier, le Read Model consiste en une projec­tion des données dans une base de données opti­mi­sée pour la lecture.

    Note : Ce qui s’est révélé utile par la suite, plusieurs écrans métiers sont plus tard deve­nus des goulots d’étran­gle­ment de l’ap­pli­ca­tion, plusieurs Read Models dédiés à ces écrans ont été déve­lop­pés,

  • Délayer au maxi­mum les écri­tures dans des queues de trai­te­ments asyn­chrones pour ne pas péna­li­ser les perfor­mances ressen­ties par les utili­sa­teurs en train d’uti­li­ser la plate­forme.

  • Réduire les trai­te­ments en un ensemble de tâches unitaires et atomiques, assu­rant un état de la base de données cohé­rent et permet­tant ainsi de paral­lé­li­ser ou délayer certains trai­te­ments.

  • Pour termi­ner, à cause de contraintes impo­sées à cette époque par l’hé­ber­geur de la solu­tion, ne pas être dépen­dant de services addi­tion­nels hors PHP et Post­greSQL.

Une des pistes envi­sa­gées mais vite aban­don­née était de partir sur un modèle d’Event sour­cing, en s’ap­pro­chant de la métho­do­lo­gie CQRS (Command Query Resource Sepa­ra­tion). Après divers essais et proto­types jetables réali­sés en amont, nous nous sommes éloi­gnés de la métho­do­lo­gie CQRS complexe à mettre en œuvre correc­te­ment. Ce choix tech­nique expé­ri­men­tal a fait persis­ter des reliquats sur le logi­ciel bien qu’ils n’aient plus de raison d’exis­ter aujour­d’hui. Nous abor­de­rons ce sujet plus tard.

Vue d’en­semble

Cette première itéra­tion était contrainte par un plan­ning serré pour la mise en produc­tion de l’ou­til demandé par le client, et par consé­quent, elle a abouti à un projet allant dans la direc­tion du DDD mais encore incom­plet. Le résul­tat a été cepen­dant satis­fai­sant car nous avons réussi à obte­nir un code stable, perfor­mant, facile à prendre en main et main­te­nir.

La maintenance
La main­te­nance | Chris­tian Bueh­ner via Unsplash

Dans une appli­ca­tion Symfony clas­sique, utili­sant la méthode appe­lée RAD (Rapid Appli­ca­tion Deve­lop­ment), il est d’usage de créer des enti­tés et de les bran­cher sur un ORM (Object-Rela­tio­nal Mapping), le plus souvent Doctrine, pour gérer leur persis­tance, ainsi que de maxi­mi­ser l’uti­li­sa­tion des compo­sants qu’ap­porte le frame­work Symfony. En bref, se donner corps et âme au frame­work et le lais­ser struc­tu­rer et porter le projet ainsi que le code métier.

Les multiples compo­sants Doctrine ainsi que son ORM forment ensemble un produit très mature, stable et perfor­mant, et l’uti­li­ser dans notre archi­tec­ture aurait été un choix perti­nent. Nous avons fait le choix de nous en sépa­rer pour d’autres raisons, qui ne seront pas détaillées dans cet article. De plus, la métho­do­lo­gie du RAD en Symfony grati­fie ses utili­sa­teurs de résul­tats spec­ta­cu­laires et d’un déve­lop­pe­ment d’ap­pli­ca­tion très rapide, tout en gardant l’ap­pli­ca­tion perfor­mante.

Il faut bien comprendre que Symfony est un frame­work très complet, qui apporte beau­coup de fonc­tion­na­li­tés. Plus il évolue dans le temps, plus il apporte de nouvelles fonc­tion­na­li­tés, et plus il devient aisé de construire une appli­ca­tion complète sans avoir à rajou­ter d’ou­tils externes ou sans avoir besoin de créer nos propres librai­ries, API ou compo­sants. Cepen­dant, le lais­ser deve­nir la colonne verté­brale d’un projet est un exer­cice à double tran­chant :

  • Dans la durée, plus on va utili­ser les faci­li­tés du frame­work, plus ses montées en version vont deve­nir diffi­ciles pour le projet, car en utili­sant ses API, on s’ex­pose à leur dépré­cia­tion éven­tuelle dans le futur ; à ce jour, le projet dont on parle a déjà vécu trois mises à jour de versions majeures de Symfony, et nous avons été confron­tés à de telles dépré­cia­tions à plusieurs reprises.

  • Si un jour une des API du frame­work dont on s’est servi pour struc­tu­rer le projet devient un goulot d’étran­gle­ment, se révèle trop limi­tée voire trop spéci­fique pour réali­ser correc­te­ment ce que le métier ou l’en­vi­ron­ne­ment tech­nique requiert, il est alors extrê­me­ment fasti­dieux de faire marche arrière.

  • Plus géné­ra­le­ment, utili­ser inté­gra­le­ment le frame­work c’est le lais­ser s’étendre et s’im­mis­cer dans le code métier, ainsi tout besoin de faire marche arrière devient presque impos­sible, car tout rempla­ce­ment d’un compo­sant du frame­work néces­si­te­rait de repas­ser sur l’in­té­gra­lité du code de l’ap­pli­ca­tion (ou d’im­plé­men­ter une couche de rétro-compa­ti­bi­lité et miser sur la migra­tion incré­men­tale).

Nous avons donc décidé de ne pas suivre les bonnes pratiques du moment et du plus grand nombre, et avons inversé la dépen­dance au frame­work pour le posi­tion­ner en tant que compo­sant remplaçable de l’in­fra­struc­ture du projet et non comme élément struc­tu­rant. Ceci afin de conser­ver un code métier le plus indé­pen­dant, portable et main­te­nable possible.

La couche Domaine

L’ap­pli­ca­tion est donc construite autour d’un noyau de code métier que nous appel­le­rons désor­mais la couche Domaine qui est décou­plée de toute brique logi­cielle exté­rieure. Elle se compose ainsi :

Les enti­tés métier

Les enti­tés métier, les modèles, équi­valent à des enti­tés que vous auriez écrites pour leur utili­sa­tion avec un ORM clas­sique, par exemple :

namespace SuperBoutique2000\Domain\Model;

class Panier
{
    public function __construct(
        private PanierId $id,
        private ClientId $clientId,
        private Collection $lignes = new ArrayCollection(),
        // ...
    ) {}
}

Les repo­si­to­ries

Les repo­si­to­ries pour lire et écrire ces enti­tés, par exemple :

namespace SuperBoutique2000\Domain\Repository;

use SuperBoutique2000\Domain\Model\ClientId;
use SuperBoutique2000\Domain\Model\Panier;
use SuperBoutique2000\Domain\Model\PanierId;

class PanierRepository extends AbstractRepository
{
    public function find(PanierId $id): ?Panier { /* ... */ }

    public function getForClient(ClientId $id): ?Panier { /* ... */ }

    public function upsert(Panier $panier): Panier { /* ... */ }

    public function get(PanierId $id): Panier { /* ... */ }
}

Dans cet exemple, les implé­men­ta­tions n’ont pas d’im­por­tance, et les méthodes vides ne sont là que pour montrer la fina­lité du repo­si­tory.

Les commandes

Les commandes, qui sont des simples DTO (Data Trans­port Object) sous la forme de classes PHP portant des proprié­tés qui repré­sentent l’en­trée utili­sa­teur, par exemple :

namespace SuperBoutique2000\Domain\Command;

use SuperBoutique2000\Domain\Model\ClientId;

class CommandeLePanierCommand
{
    public function __construct(
        public readonly ClientId $clientId,
        public readonly bool $videPanier = true,
    ) {}
}

Les hand­lers

Les hand­lers qui sont des fonc­tions service qui sont écrites sous la forme de méthodes de classes PHP dont le but est d’in­gé­rer et de trai­ter les commandes, par exemple :

namespace SuperBoutique2000\Domain\Handler;

use SuperBoutique2000\Domain\Command\CommandeLePanierCommand;
use SuperBoutique2000\Domain\Command\CommandeCreeResponse;
use SuperBoutique2000\Domain\Repository\CommandeRepository;
use SuperBoutique2000\Domain\Repository\PanierRepository;
use MakinaCorpus\CoreBus\Attr\CommandHandler;

class CommandeHandler
{
    public function __construct(
        private readonly CommandeRepository $commandeRepository,
        private readonly PanierRepository $panierRepository,
    ) {}

    #[CommandHandler]
    public function commandePanier(CommandeLePanierCommand $command): void
    {
        $commande = Commande::fromPanier(
            $this
                ->panierRepository
                ->getForClient(
                    $command->clientId
                ),
        );

        return new CommandeCreeResponse(
            $this
                ->commandeRepository
                ->upsert($commande)
                ->getId(),
        );
    }
}

Pour aller plus loin

À noter que lorsque des trai­te­ments métier sont plus complexes, ils sont extraits sous la forme de services addi­tion­nels, ce qui permet d’iso­ler cette complexité tout en la rendant plus faci­le­ment testable.

L’in­té­gra­lité des règles métier et de l’es­pace fonc­tion­nel de l’ap­pli­ca­tion est présent dans la couche Domaine, et ne repose sur stric­te­ment aucun code prove­nant de l’ex­té­rieur ; à l’ex­cep­tion du compo­sant de connexion à la base de données, car dans ce premier projet, les repo­si­to­ries n’ont pas été décou­plés de leur implé­men­ta­tion dans la couche Infra­struc­ture.

Dans ce projet, nous avons fait le choix d’uti­li­ser notre propre DBAL (Data­Base Access Layer) et non un prove­nant de la commu­nauté Open Source, pour de nombreuses raisons qui pour­raient être détaillées dans un autre article. Utili­ser ici un ORM à la place d’un connec­teur SQL spéci­fique aurait égale­ment été un choix légi­time et valide, qui n’au­rait pas eu de consé­quences sur les choix de concep­tion de l’ap­pli­ca­tion.

Toutes les possibles dépen­dances externes dont le domaine métier pour­rait avoir besoin sont abstraites par des services sous la forme d’in­ter­faces, et leurs implé­men­ta­tions écrites en dehors de cette couche. Les implé­men­ta­tions réelles sont injec­tées aux compo­sants en ayant besoin via un conte­neur d’injec­tion de dépen­dance. Cepen­dant dans ce projet, il y en a très peu.

Le frame­work

Le code du domaine métier ne se suffit pas à lui-même, il nous fallait de l’ou­tillage pour ce qui est de l’ordre de l’in­fra­struc­ture :

  • La gestion du proto­cole HTTP, et donc des requêtes et réponses

  • La gestion de l’au­then­ti­fi­ca­tion et de la sécu­rité

  • Un compo­sant pour créer et main­te­nir des formu­laires, une des parties les plus complexes de ce genre d’ap­pli­ca­tion

  • Un compo­sant d’injec­tion de dépen­dances pour auto­ma­ti­ser la confi­gu­ra­tion et ne pas avoir à s’en soucier

Maitri­sant le frame­work Symfony, nous avons décidé de l’uti­li­ser pour toute la partie infra­struc­ture tech­nique, confi­gu­ra­tion et inter­face utili­sa­teur.

Il est impor­tant de noter ici que le frame­work Symfony, dans ce projet, ne consti­tue pas une base tech­nique sur laquelle repose l’ap­pli­ca­tion, mais est lui même un compo­sant discret dans l’in­fra­struc­ture : l’ap­pli­ca­tion a été conçue de telle sorte que le rempla­ce­ment de Symfony par un autre outillage reste possible.

Image
RAD vs Clean Architecture
Inver­sion de la dépen­dance au frame­work.

En réalité, bien que nous ayons comme possi­bi­lité de rempla­cer le frame­work, cette opéra­tion ne serait pas triviale : des compo­sants, comme les contrô­leurs, utilisent Symfony et par consé­quent, pour rempla­cer le frame­work, il faudrait aussi réécrire l’in­ter­face utili­sa­teur.

Cepen­dant, de la façon dont est construite l’ap­pli­ca­tion, on a conservé le Domaine métier complè­te­ment décou­plé du frame­work, ce qui lui permet d’être réuti­li­sable quel que soit ce dernier, permet­tant une hypo­thé­tique évolu­tion majeure du produit vers un autre outillage sans avoir à toucher au Domaine.

L’in­ter­face utili­sa­teur

Toute l’in­ter­face utili­sa­teur est construite sous la forme de contrô­leurs Symfony et retournent des réponses HTML sous la forme de template Twig, tout ce qu’il y a de plus clas­sique.

Cepen­dant, nous nous éloi­gnons un peu d’une appli­ca­tion clas­sique au travers des conven­tions mises en place déduites du pattern CQS :

  • Les infor­ma­tions affi­chées à l’écran, donc resti­tuées au travers des templates Twig, proviennent des repo­si­to­ries et sont utili­sées dans ce contexte en lecture seule et sont faci­le­ment remplaçables par des Read Model.

  • Tout envoi de requête de modi­fi­ca­tion de données, donc des requêtes HTTP PATCH, POST ou PUT (étant une appli­ca­tion web tradi­tion­nelle, ce ne sera dans notre cas que du POST) via un contrô­leur Symfony donne lieu à la créa­tion d’un objet commande et à son envoi dans le bus de messages.

Ce qui se passe ensuite est la consom­ma­tion du message par la couche Domaine (par les hand­lers, qui remplacent les contrô­leurs) et reste donc forte­ment décou­plé de toute dépen­dance externe.

Le bus de messages

Comme vu précé­dem­ment, nous avons fait le choix de conce­voir l’ap­pli­ca­tion autour d’un bus de messages pour gérer les opéra­tions d’écri­ture. À l’époque où nous avons démarré la réali­sa­tion de ce projet, nous avions plusieurs possi­bi­li­tés :

  • Utili­ser le compo­sant symfony/messenger que nous four­nit le frame­work Symfony que nous avons évalué et testé exten­si­ve­ment.

  • Utili­ser un bus tel que RabbitMQ ou autres préten­dants, et utili­ser un connec­teur exis­tant pour commu­niquer avec lui.

  • Déve­lop­per notre propre solu­tion.

Boîtes aux lettres
Le bus de message | Mathyas Kurmann via Unsplash

Nous avons aban­donné le compo­sant Messen­ger de Symfony car il était encore loin d’ar­ri­ver à matu­rité à ce moment. À ce jour, il est devenu un compo­sant mature et stable, mais ayant choisi histo­rique­ment une autre voie en déve­lop­pant notre propre outillage, nous avons conforté le choix de ne pas y migrer. À noter que si c’était à refaire dans le temps présent, nous utili­se­rions le compo­sant Messen­ger sans hési­ter.

La solu­tion d’uti­li­ser un serveur tiers tel que RabbitMQ a été très sérieu­se­ment envi­sa­gée. Ainsi, nous avons testé divers proto­types autour de ce dernier, mais à cause de contraintes tech­niques liées au futur envi­ron­ne­ment de produc­tion, nous avons dû aban­don­ner cette piste.

Nous avons donc déve­loppé notre propre solu­tion de bus de messages bran­chée par défaut sur Post­greSQL. Il fonc­tionne aujour­d’hui dans trois projets distincts, ce bus de messages a été stabi­lisé et amélioré à travers le temps.

Commandes et événe­ments

Une petite erreur de concep­tion

Une des erreurs de ce projet a été de vouloir trop se rappro­cher d’un modèle de stockage en Event Sour­cing sans s’y enga­ger complè­te­ment. Le but du tout premier proto­type était de permettre de rejouer les commandes passées dans le système, pour repro­duire un état stable à tout moment.

Cepen­dant, vouloir se rappro­cher de ce but sans prendre le virage vers la métho­do­lo­gie CQRS, a malheu­reu­se­ment enfanté une petite chimère assez impro­bable :

  • Nous avons donc des commandes qui sont envoyées dans le bus.

  • À chacune de ses commandes, nous ratta­chons un iden­ti­fiant qui permet de retrou­ver l’objet métier auquel est desti­née cette commande.

  • Une fois la commande termi­née, elle est stockée dans un Event Store.

  • Si une erreur est surve­nue, elle est stockée dans l’Event Store de la même manière, mais avec une copie de la trace de l’ex­cep­tion qui s’est produite.

Vous pouvez tout de suite voir là où le bât blesse : nous ne stockons pas des événe­ments, mais les commandes. Notre Event Store placé ici à l’ori­gine pour de grands dessins est devenu à la fin un histo­rique géant de tout ce qui se passe sur la plate­forme, mais il n’a aucune utilité métier, il n’existe que pour exis­ter.

Par chance, ce modèle nous a apporté une chose que l’on avait pas au début imagi­née : nous avons un jour­nal d’au­dit complet de ce qu’il se passe sur la plate­forme. Ce point nous a permis d’al­ler plus vite dans la réso­lu­tion des problèmes, car les faci­li­tés de listing et de filtrage que nous apporte cet Event Store se sont alors substi­tuées à un agré­ga­teur de logs.

Pour cette dernière raison, nous avons décidé de le conser­ver, au prix de devoir déve­lop­per des tâches plani­fiées pour le nettoyer régu­liè­re­ment et ne garder qu’un histo­rique récent de quelques mois.

Et les Domain Event ?

En prin­cipe, un Domain Event est un événe­ment interne dans une appli­ca­tion, lancé lorsqu’une action se produit dans un certain contexte appli­ca­tif, permet­tant à un autre contexte appli­ca­tif de réagir lorsqu’il se produit.

Dans le Domain Driven Design, l’ap­pli­ca­tion est décou­pée en Boun­ding Context, notion qui relate un espace fonc­tion­nel parti­cu­lier, et indé­pen­dant des autres espaces fonc­tion­nels. Les Domain Events permettent de traver­ser les fron­tières (Cros­sing Boun­da­ries), c’est-à-dire sortir d’un espace fonc­tion­nel pour déclen­cher des actions dans un autre, sans coupler les espaces fonc­tion­nels autre­ment qu’avec l’évé­ne­ment lui-même.

Notre appli­ca­tion ne dispose pas de Domain Events, dans ce projet en parti­cu­lier, il y a un et un et un seul métier, et par consé­quent un et un seul Boun­ding Context, ce qui rend nulle l’uti­lité d’avoir des Domain Events.

Quelques années plus tard, dans le cadre d’une évolu­tion assez consé­quente, nous avons utilisé les commandes exis­tantes et permis à des Event Liste­ner de réagir à leur trai­te­ment, et ce dans la même tran­sac­tion métier, afin de créer cette fonc­tion­na­lité. Ceci nous a permis d’iso­ler des parties de code complexes dans leur propre espace fonc­tion­nel.

Conclu­sion

Ce premier projet nous a permis d’amor­cer le virage vers un modèle se rappro­chant de la métho­do­lo­gie Domain Driven Design, et de se faire la main avec un modèle d’écri­tures asyn­chrones via un bus de messages. Il n’est cepen­dant pas exempt de défauts.

Voici ce que nous avons obtenu lors de cette première itéra­tion :

  • Des enti­tés métier et leurs repo­si­to­ries, construits sur un connec­teur SQL qui a été réalisé dans le cadre de ce projet, main­tenu par nos soins.

  • Un bus de messages inté­gré à l’ap­pli­ca­tion, repo­sant sur Post­greSQL, dont implé­men­ta­tion initiale a été créée pour ce projet, qui à ce jour n’a plus rien à voir avec son implé­men­ta­tion d’ori­gine et qui est utilisé dans d’autres projets.

  • Toutes les lectures sont synchrones, et utilisent direc­te­ment les repo­si­to­ries depuis l’in­ter­face utili­sa­teur.

  • Toutes les écri­tures sont asyn­chrones de part leur API, et passent par des commandes envoyées dans le bus depuis l’in­ter­face utili­sa­teur. À noter que dans certains contextes, le bus est exécuté de façon synchrone, ce qui n’a pas d’im­pact sur le décou­plage de la couche Domaine.

  • La gestion des tran­sac­tions SQL est inté­grée sous la forme de déco­ra­teur du bus de messages, qui agit en cas d’échec de séria­li­sa­tion de la base en passant les messages en retry dans le bus, ce qui permet de contour­ner les scéna­rios de conten­tion liés à des tran­sac­tions qui se marchent sur les pieds dans la base de données SQL auto­ma­tique­ment. Ça peut paraître primi­tif, mais après 5 ans de produc­tion, ce méca­nisme n’a pas failli à sa tâche.

  • Le code métier est inté­gra­le­ment indé­pen­dant de toute dépen­dance externe, à l’ex­cep­tion de la construc­tion des requêtes SQL dans les repo­si­to­ries.

  • Toute la vali­da­tion est faite dans les Command Hand­ler, et donc dans la couche Domaine, ou réside le métier final du client, elle n’uti­lise pas d’API tierce, elle est orien­tée métier et écrite en PHP vanilla.

  • Le frame­work n’est pas struc­tu­rant pour le code métier, et est relé­gué dans une couche infra­struc­ture. Seuls les contrô­leurs Symfony sont direc­te­ment influen­cés et struc­tu­rés par ce dernier.

  • Nous dispo­sons d’un reliquat d’Event Store qui jour­na­lise l’in­té­gra­lité des commandes passant sur la plate­forme, ce qui sert à mener des audits sur la santé de la plate­forme.

  • Nous n’avons pas de Domain Events dans ce projet, bien qu’ils aient été intro­duits plus tard dans une future version, sous la forme d’un contour­ne­ment de l’Event Store.

Bien que souf­frant de quelques défauts de concep­tion, ce projet conti­nue aujour­d’hui de vivre en produc­tion, est régu­liè­re­ment main­tenu, et va bien­tôt fêter sa version 5.0. Il a subit trois évolu­tions fonc­tion­nelles majeures, au cours des cinq dernières années, et conti­nue à fonc­tion­ner de façon perfor­mante et rési­liente dans un envi­ron­ne­ment maté­riel modeste, n’ayant pas été remplacé depuis son lance­ment initial.

Par la suite, et travaillant pour le même client, nous avons conti­nuer de faire évoluer la concep­tion présen­tée ici. Un second projet qui fait évoluer cette archi­tec­ture sera présen­tée dans un prochain article.

Formations associées

Formation Symfony

Formation Symfony Initiation

Paris Du 28 au 30 mai 2024

Voir la formation

Formations Outils et bases de données

Formation PostgreSQL

Nantes Du 29 au 31 janvier 2024

Voir la formation

Actualités en lien

Image
Symfony
11/04/2024

Access Control : une biblio­thèque PHP pour gérer des droits d’ac­cès

Suite à un projet de gestion métier opéra­tion­nel dont la durée de vie et la main­te­nance sont à long termes, nous avons expé­ri­menté un passage de celui-ci sur l’archi­tec­ture hexa­go­nale et la clean archi­tec­ture.

Voir l'article
Image
Encart article 2 : Itéra­tions vers le DDD et la clean archi­tec­ture avec Symfony
20/02/2024

Itéra­tions vers le DDD et la clean archi­tec­ture avec Symfony (2/2)

Quels virages avons-nous pris après un premier projet expé­ri­men­tal pour stabi­li­ser notre concep­tion logi­cielle, et que ferons-nous plus tard ?

Voir l'article
Image
Symfony + Vue.js
21/06/2022

Créer une application Symfony/Vue.js

Pour faire des essais ou bien démarrer un nouveau projet, vous avez besoin de créer rapidement une application Symfony couplée avec un front Vue.js ? Suivez le guide !

Voir l'article

Inscription à la newsletter

Nous vous avons convaincus