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

Nantes Du 25 au 27 mars 2025

Voir la formation

Formations Outils et bases de données

Formation PostgreSQL

À distance (FOAD) Du 12 au 14 novembre 2024

Voir la formation

Actualités en lien

Image
Encart blog DBToolsBundle
18/07/2024

DbTools­Bundle : sortie de la version 1.2

Décou­vrez les nouveau­tés de cette nouvelle version ainsi que les fonc­tion­na­li­tés à venir de la prochaine version majeure.

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