alexandre

Petite astuce, car non documentée officiellement, concernant Sylius. Si comme moi vous créez/utilisez des plugins, vous avez dû vous apercevoir que les nouveaux éléments de menus apparaissaient toujours en dernier.

Ce comportement est normal car il s'agit de listeners qui sont appelés les uns à la suite des autres en suivant l'ordre de déclaration. L'astuce consiste donc à créer un listener qui sera toujours appelé en dernier et qui ne servira qu'à modifier l'ordre des items.

Dans un premier temps, on va créer le listener.

<?php

declare(strict_types=1);

namespace App\Menu\Event;

use Sylius\Bundle\UiBundle\Menu\Event\MenuBuilderEvent;

final class AdminMenuListener
{
    public function reorderMenu(MenuBuilderEvent $event): void
    {
        $menu = $event->getMenu();
        $menu->reorderChildren([
            'catalog',
            'foo_menu', // plugin foo
            'bar_menu', // plugin bar
            'sales',
            'customers',
            'marketing',
            'configuration',
        ]);
    }
}

Puis on déclare la configuration de ce listener en pensant à lui donner l'ordre de priorité le plus bas (tous les éléments du menu doivent être connus avant d'être réordonnés).

services:
    App\Menu\Event\AdminMenuListener:
        tags:
            - { name: kernel.event_listener, event: sylius.menu.admin.main, method: reorderMenu, priority: -256 }

And voilà !

Attention :

  • Cet article fait partie d’une série consacrée à de grands principes d’architecture qui me sont propres et a pour objectif de faire comprendre aux tiers comment évoluer sur mes propres projets.
  • Cet article évolue suivant les retours et les différentes expériences.
  • Le terme Domain n'implique pas l'obligation d'une conception orientée DDD.

Le design pattern ADR est basé sur le triptyque ActionDomainResponder. Il est l’une des alternatives au design pattern MVC.

Il est possible de vulgariser le pattern de la façon suivante :

  • Action : l’équivalent d’un contrôleur, dédié à une opération HTTP (une route) de l’application ;
  • Domain : le point d'entrée logique vers le code métier ;
  • Responder : en charge du traitement et du rendu de la réponse.

Pourquoi ?

Le pattern MVC invite à créer des controllers composés d'une ou plusieurs actions. A l'usage, ces controllers peuvent devenir conséquents voir problématiques car trop complexes. L'ADR permet de limiter cette complexité globale. Il sera toujours possible d'avoir du code complexe, mais cette complexité sera limitée à une action dans un contexte précis.

Étant donné que l'ADR est composé d'un triptyque, le pattern encourage naturellement la POO et un SRP simple dans le sens ou les traitements métiers seront plus naturellement délégués à des “services”.

Enfin, l'ADR via son Responder assure une cohérence dans le rendu des réponses de l'application.

Comment ?

Si une Action est sensiblement la même chose qu’un controller à savoir être en charge du traitement de la requête entrante, le Responder peut quant à lui être conçu de manière différente.

Le moyen le plus simple consiste à mutualiser les Responder en les dédiant à un type de réponse. Dans une application HTTP classique, on pourra ainsi créer différents types de Responder :

  • HtmlResponder ;
  • RedirectResponder ;
  • DownloadResponder ;
  • JsonResponder ;
  • XmlResponder ;
  • ...

Plus complexe, mais conforme à la définition initiale du pattern, le ou la développeur·euse dédiera une action donnée à un Responder unique. On utilisera alors cette spécialisation pour les cas où le formatage de la réponse est complexe.

Afin de conserver la qualité de notre application, ce Responder pourra encapsuler un Responder commun (via la composition).

Enfin, l'utilisation la plus complexe consistera quant à elle à ajouter un Payload afin de spécialiser la réponse ainsi que son contenu.

Dans une application orientée DDD, l'Action et le Responder seront situés dans la couche User Interface. L'emplacement de la couche Domain dépend quant à elle des choix d'architecture de l'application. N'y voyez, une fois de plus, qu'un terme commun à d'autres approches de conception.

Exemples

<?php

namespace App\UI\Front\Action;

use App\UI\Front\Responder\HelloResponder;
use App\Application\Service;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Message\ResponseInterface;

final class HelloAction
{
    private Service $service;

    private HelloResponder $responder;
    
    public function __construct(
        Service $service, 
        HelloResponder $responder
    ) {
        $this->service = $service;
        $this->responder = $responder;
    }
    
    public function __invoke(ServerRequestInterface $request): ResponseInterface
    {
        // 1. entrée
        $data = (string) $request->getBody();
        
        // 2. traitement (Domain)
        $service = ($this->service)($data);
        
        // 3. sortie (Responder)
        return ($this->responder)($service);
   }
}
<?php

namespace App\UI\Front\Responder;

use App\Application\Service;
use Psr\Http\Message\ResponseInterface;
use Zend\Diactoros\Response\HtmlResponse;

final class HelloResponder
{   
    public function __invoke(Service $service): ResponseInterface
    {       
        $response = $service->getData();
        
        return new HtmlResponse($response);
   }
}

Symfony

L'utilisation de l'ADR n'est pas mise en avant dans Symfony. Il faudra donc veiller à sensibiliser l'équipe à ce sujet. A contrario, l'autowiring de Symfony étant performant, il est possible d'utiliser l'ADR sans aucune modification particulière du framework. Il n'y a donc aucune contrainte technique.

De par sa conception, on évitera d'injecter les différents services utilisés par une Action dans la méthode invoke et on privilégiera l'injection via le constructeur.

Si l'utilisation de Twig n'a pour le moment jamais été remise en question par les auteurs de Symfony, des changements dans les best practices ont eu lieu à plusieurs reprises sur les controllers. L'utilisation de l'ADR a pu permettre de limiter l'impact lors des migrations. Il est concrètement possible de les implémenter depuis la version 2.7 de Symfony.

Questions/Réponses

Aucune TwitterEmail

Conclusion

L'ADR est une alternative très intéressante au MVC et adaptée au flux Request/Response. Le coût technique de son utilisation n'est pas important, mais son impact est fort. C'est également un cadre plus strict avec un fort pouvoir d'adaptation.

Certain·e·s lui reprocheront sont coût cognitif avec comme argument principal l'impact sur la directory structure du projet (il existe un fichier par action), mais il est alors facile de répondre que ce coût est largement compensé lorsque l'on visualise le code.

Pour terminer, cette segmentation permet également de faciliter les tests unitaires.

Attention :

  • Cet article fait partie d’une série consacrée à de grands principes d’architecture qui me sont propres et a pour objectif de faire comprendre aux tiers comment évoluer sur mes propres projets.

  • Cet article évolue suivant les retours et les différentes expériences.

  • User Interface fait partie des termes utilisés en DDD. N'y voyez cependant qu'un terme commun et une inspiration (forte mais une inspiration tout de même).

Le terme Interface Utilisateur (User Interface ou encore UI) désigne les différents moyens d’une application de communiquer avec un système extérieur : humain ou machine.

On trouve différents ensembles d’Interfaces Utilisateur :

  • HTML
  • API
  • Console
  • Message Queue
  • ...

Tous ces ensembles peuvent eux-mêmes être divisés. Une Interface Utilisateur HTML pouvant par exemple représenter la partie vitrine d’un site internet et une autre Interface Utilisateur l’administration. Autre exemple, une Interface Utilisateur API peut quant à elle exister au travers de différentes versions clairement identifiées.

Pourquoi ?

Dans un système complexe et destiné à évoluer, il est plus simple de segmenter ses différentes Interfaces Utilisateur et de les identifier clairement.

Cette segmentation est aussi bien visuelle : les éléments d’une Interface Utilisateur sont regroupés dans un même package, que technique : il est plus facile de leur donner des prérequis différents, de les faire évoluer dans le temps ou encore de les déprécier.

Une Interface Utilisateur est également décorrélée des autres couches de l'application (qui sont quant à elles chargées du traitement métier). Cela sous-entend que différentes Interfaces Utilisateur effectuant le même traitement encourageront naturellement le ou la développeur·se à utiliser des services identiques et les bonnes pratiques SOLID.

Les Interfaces Utilisateur peuvent également fonctionner de manière autonome avec des données de test ou temporaires et simuler un traitement sans impliquer d’autres services (via de la donnée brute par exemple).

Supprimer une Interface Utilisateur ne devrait pas avoir d'impact sur les autres Interfaces Utilisateur mais seulement sur les systèmes extérieurs en lien avec l'Interface Utilisateur (exemple : moteur de recherche, visiteur...).

Exemples

<?php

namespace App\UI\Front\Action;

use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Message\ResponseInterface;
use Zend\Diactoros\Response\HtmlResponse;

final class Hello
{
    private Service $service;
    
    public function __construct(Service $service)
    {
        $this->service = $service;
    }
    
    public function __invoke(ServerRequestInterface $request): ResponseInterface
    {
        // 1. entrée
        $data = (string) $request->getBody();
        
        // 2. traitement
        $response = $this->service->__invoke($data);
        
        // 3. sortie
        return new HtmlResponse($response);
   }
}
<?php

namespace App\UI\Console\Command;

use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;

final class HelloCommand extends Command
{
    protected static $defaultName = 'app:hello';

    private Service $service;
    
    public function __construct(Service $service)
    {
        $this->service = $service;

        parent::__construct();
    }
    
    protected function configure()
    {
        // arguments
    }
    
    protected function execute(InputInterface $input, OutputInterface $output): ResponseInterface
    {
        // 1. entrée
        $data = $input->getArgument('foo');
        
        // 2. traitement
        $response = $this->service->__invoke($data);
        
        // 3. sortie
        $ouput->writeln($response);

        return Command::SUCCESS;
   }
}

Éléments annexes

Tous les éléments en lien avec l’extérieur font partie de l'Interface Utilisateur.

Cela implique par exemple les formulaires. On créera ses formulaires (et les éléments relatifs à son traitement) dans un namespace tel que App\UI\{...}\Form\{DTO,Type,Handler,...}.

A propos des formulaires, ces derniers font partie de l'Interface Utilisateur car ils sont intimement liées aux données entrantes, données qui seront ensuite traitées par les autres couches de l'applications. Cela introduit un potentiel lien avec un composant tiers (comme le composant Form de Symfony) mais ce lien, dans ce cas précis, n'est actuellement pas considéré comme problématique car la valeur ajoutée globale de l'Interface Utilisateur est plus importante.

Dans le cas de l'intégration dans un système tiers (plugin, extension, surcouche...), il est conseillé de considérer l'outil principal comme une Interface Utilisateur. Cette considération permettra alors de segmenter clairement la partie du code en relation avec l'outil et le code métier ajouté. Une fois encore, le changement d'outil et donc d'Interface Utilisateur ne doit pas impacter le code métier.

Symfony

Certaines règles sont en désaccord avec les bonnes pratiques officielles Symfony concernant les formulaires :

  • L’Interface Utilisateur encourage l’utilisation des Data Transfer Object et un traitement en deux temps (les traitements backend devraient être agnostiques des Interfaces Utilisateur) ;
  • Dans le cas d’un même formulaire évoluant avec les mêmes données sources dans deux Interfaces Utilisateur, on encouragera la duplication du Type ;
  • Sur la base du point précédent, compte tenu de la faible probabilité d’avoir deux Interfaces Utilisateur remplissant le même rôle avec les mêmes données sources, on déconseillera l’utilisation des Extensions sur autre chose que des types primitifs fournis par le framework et/ou des bundles. Si de telles extensions doivent être créées, elles ne doivent pas faire partie du package UI mais de l'Infrastructure.

Questions/Réponses

Aucune TwitterEmail

Conclusion

L’Interface Utilisateur est l’une des couches composant une application. Il est important de la traiter aussi bien que le reste de l’application, car c’est celle qui, dans le temps, pourra être constituée de code spaghetti. Charge à vous, sur cette base, de trouver votre meilleure façon de faire, qu’il s’agisse d’un modèle de type MVC, ADR ou maison.

TL;DR : une note (très personnelle) concernant mon parcours de conférencier avec un petit teaser concernant l'avenir.

Au commencement

J'ai commencé à donner des conférences avec l'AFUP (pour l’antenne de Lyon) en 2014. Je faisais 20kg de moins, j'étais rasé de près et j'évoquais alors l’utilisation de la sémantique dans le but de faciliter la conception du modèle d’un projet. J'ai ensuite rapidement enchaîné sur le sujet de l’intérêt d’ouvrir ses process au travers d’une conférence sur l’entreprise Open Source.

Ma première grosse conférence au Forum PHP parlait quant à elle de l’organisation de projets DDD. J’ai organisé, à peu près au même moment, le DDD-day à Lyon. Il y a eu ensuite différents REX [1] [2] sur des antennes locales et au travers de différents Meetups/BBL pour terminer sur la récente conférence de l’AFUP Day consacré à la programmation défensive.

Entre temps j’ai parlé d’autres sujets, mais toujours très peu de technique. Pour rester factuel, environ 90 % de mes conférences techniques ont eu lieu sur Bitcoin et sur la création de Smart Contract Ethereum. Le tout avec différents niveaux de technicité suivant le public présent. Je n’ai jamais réussi à parler de tout ça dans l’écosystème PHP (sauf chez M6Web), c'est officiellement mon seul regret jusqu'à maintenant.

L'avenir

Je ne proposerai plus de conférences sur mes thématiques de prédilections. Je pense avoir dit ce que j’avais à dire, plus ou moins bien, et tout ça sera encore valable pendant quelques années.

Si tout se passe bien, l'avenir sera consacré à des applications réelles, des choses concrètes, des choses que l’ont peut voir et toucher du doigt (vous l’avez ?) et d’autres idéaux.

Je sais que c'est encore un peu (trop) vague. Certains sujets mettent du temps à être posés et nécessitent énormément de travail en amont. J'ai commencé en début d'année, confinement, nécessité de se mettre à fond pour rattraper le temps. Ça arrivera forcément à un moment.

En attendant

J’avais annoncé en début d’année que je me tenais à disposition des antennes AFUP locales et de tous les autres. C'est toujours le cas et j’aurais bien entendu immensément de plaisir à retravailler ces sujets que je ne quitte pas professionnellement (bien au contraire) puis à vous les présenter. N'hésitez pas à venir en discuter.

Pour conclure, j'espère sincèrement que l'on pourra tous se voir au Forum PHP et que coco le virus ne viendra pas nous embêter. A bientôt ❤️.

J'ai laissé un peu d'eau couler sous les ponts et ai finalement décidé de partir sur deux possibilités :

  • Utiliser “main” pour la majorité de mes projets. Un consensus a émergé très rapidement autour du terme qui est suffisamment explicite à mon gout ;
  • Utiliser l'option proposée par Fabien Potencier pour les projets plus conséquents.

Deux précisions tout de même.

Je pense avoir donné mon point de vue au travers de différentes discussions. Cela fait très longtemps que je pense que la société ne va pas dans le bon sens. J'en discute régulièrement et énormément avec certaines personnes. Je suis comme beaucoup très embêté que cela aille jusqu'à la remise en question de termes techniques, MAIS le fait est que cela me semble nécessaire à bien des égards et que toutes les discussions autour de ce sujet ont été utiles pour certain.e.s. Si changer le nom de mes branches me permet d'avoir encore de nouvelles discussions autour du sujet et que ces discussions permettent à un peu plus de personnes de prendre conscience des choses, ça me convient.

À l'opposé, penser que l'on règle le problème en renommant sa branche principale sur GIT est aussi stupide que d'illustrer ce changement avec des extrêmes qui n'ont rien à voir. Le monde du web est trop peu politisé et je pense que cela n'est pas une bonne chose. Nous avons la capacité de faire beaucoup plus grâce à nos lignes de code et le temps est venu d'endosser ce rôle.

J'espère sincèrement que vous prendrez le temps d’élever le débat dans votre for intérieur et que vous vous intéresserez aux “vrais” problèmes, ceux qui sont liés à ces discussions. Ils existent, ils ont besoin d'être adressés et nous sommes en capacité d'essayer de les résoudre.

TL;DR : J'ai profité du temps de confinement pour continuer à monter en compétence sur Sylius. Différents plugins ont été créés pour couvrir des besoins dans une boutique que je gère mensuellement. Le premier à être release est le plus simple : un plugin Cookie Alert.

Sylius est une solution ecommerce basée sur le framework PHP Symfony. Cette solution se veut simple, efficace et n'a pour objectif que de couvrir le besoin de la vente en ligne. Encore jeune, Sylius ne possède pas tout à fait un écosystème de plugin aussi étoffé que ses concurrents (Prestashop/Magento en tête), mais si tout avance comme prévu dans la roadmap, cet écart est destiné à se combler. Chose intéressante, beaucoup d'ecommercants et de sociétés spécialisées sur Magento se tournent vers Sylius.

Étant un habitué de Sylius depuis longtemps, j'ai pu goûter aux joies des migrations à de multiples reprises et ai toujours réagi suivant les besoins et demande des projets. L'un des projets a récemment été mis à jour et ayant un peu de temps, confinement oblige, j'ai pris le parti de pleinement embrasser la version 1.7 et ses modifications.

Nouvelle organisation des entités

Les précédentes versions de Sylius vous invitaient à étendre les entités à la demande vous obligeant à créer des fichiers, de la configuration et au moindre oubli, vous sanctionner avec des erreurs Doctrine.

Sylius 1.7 change un peu les choses, les entités sont déjà étendues et la configuration par défaut est écrite. Ça ne parait rien comme ça, mais coté DX, c'est quand même beaucoup plus simple. Changement adopté.

Semantic UI + Gulp vs Bootstrap + Encore

Pour faire simple, je n'aime vraiment pas Semantic UI et Gulp. Je ne vois pas quoi ajouter. N'étant pas un grand fan de JavaScript et ayant toujours eu mieux à faire que de m'intéresser réellement à tout ça (hormis une vraie tentative avec React), j'ai vraiment regretté le choix de Semantic UI lorsque je souhaitais ajouter des dépendances JavaScript car tout ce que j'avais l'habitude de faire avec Bootstrap était à reprendre.

Sylius 1.7 ouvre officiellement la piste de Bootstrap et Encore. Petit couac sur le theme Bootstrap en version 1.7, il n'est pas encore entièrement compatible (spoil : je me suis mis ce point sur ma todolist). C'est donc avec beaucoup de joie que j'ai greffé Bootstrap, migré tout mon JavaScript sur Encore et... Bonheur.

Themes

Je n'utilisais pas les thèmes, j'ai révisé ma position au passage en créant un thème enfant à celui de bootstrap. Aucun problème particulier, je vous conseille de vous y mettre.

Événements de thèmes

Sylius a revu sa copie à ce niveau en passant de SonataBlock à son propre système de block, quasiment identique, ayant pour but de pouvoir manipuler son interface utilisateur de manière plus simple. J'en ai beaucoup discuté avec la personne en charge de la boutique, on a parlé des avantages et des inconvénients, comparé avec la concurrence (notamment Prestashop) et nous en sommes venu à la conclusion que sur le long terme, il y avait un réel intérêt à se mettre au plus tôt à l'utilisation des événements.

Plugins

Comme beaucoup, j'écris mon code dans src/ et l'organise en conséquence. J'ai décidé de me mettre sérieusement aux plugins car je pense que l’écosystème en a réellement besoin et qu'il y a vraiment des choses à faire, certes plus ou moins intéressantes, mais il y a à faire.

Plusieurs plugins ont donc été créés dont ce premier plugin, très bête, dont nous allons parler.

SyliusCookieAlertPlugin

CookieAlert a pour objectif simple d'afficher un bandeau en bas de page pour informer de l'utilisation de cookies. Ni plus ni moins. Ce plugin utilise une dépendance JavaScript et les événements pour s'ajouter au bon endroit avec un simple template. Et c'est tout (pour le moment).

Il m'a par contre permis de remonter différents points :

  1. La stack de test des plugins est une plaie pour moi qui ai quasiment tout dockerisé sur mon ordinateur. Ce plugin qui modifie uniquement l'UI du front n'est donc pas testé et je ne le considère pas stable le temps de régler ce problème d'une manière ou d'une autre.

  2. Je ne sais pas encore quel serait le meilleur moyen pour prémâcher le travail d'intégration des dépendances JavaScript et si créer deux fichiers SASS/JS, destinés à être consommés par Encore est réellement une bonne idée.

Installation

Rien de très compliqué, il y a aura une PR pour rendre le plugin compatible avec flex dès qu'il sera stable.

  • Ajouter le plugin dans son composer.json

composer require black/sylius-cookie-alert-plugin:^1.0.0@dev

  • Activer le plugin :
<?php

// config/bundles.php

return [
    // ...
    Black\SyliusCookieAlertPlugin\BlackSyliusCookieAlertPlugin::class => ['all' => true],
];
  • Importer la configuration
# config/packages/sylius_cookie_alert.yaml
imports:
    - { resource: "@BlackSyliusCookieAlertPlugin/Resources/config/app/config.yml" }
  • Installer la dépendance JavaScript yarn add bootstrap-cookie-alert
  • Ajouter la dépendance dans son fichier principal :
  require('bootstrap-cookie-alert/cookiealert');
  • Ajouter le css du plugin (j'utilise SASS)
@import '~bootstrap-cookie-alert/cookiealert.css';

Rendu final

Le résultat

Sponsoring & contributions

Ce plugin est officiellement sponsorisé par Vanoix qui m'a rémunéré pour la création de ce plugin mais aussi de tous les autres. Nous aurons l'occasion d'en reparler très bientôt.

Vos bonnes idées sont bien entendu les bienvenues. Ce plugin ne parait rien comme ça, mais il reste un indispensable et pourrait servir de base à quelque chose de beaucoup plus conséquent si l'on prend en compte tout ce qu'il y a à faire autour du RGPD.

TL ; DR : je ne suis pas un grand fan de la manipulation de mes entités avec des mutateurs du type set*. L’utilisation du composant workflow avec cette contrainte n’est pas possible dans son comportement par défaut et… C’est embêtant. Heureusement, ce « problème » se corrige avec deux petits fichiers.

Le composant workflow est un composant créé par Grégoire Pineau dont l’objectif est de mettre en place « une machine à état ». Pour faire simple, cela permet de stocker l’état d’un objet via une propriété afin de pouvoir valider puis appliquer des modifications sur cet objet. Si vous n’avez jamais utilisé de machine à état, vous devez certainement manipuler beaucoup de conditions et comme vous avez pu vous en apercevoir, ce n’est pas une bonne pratique sur le long terme.

Pour résumer au plus simple, une machine à état fonctionne de la façon suivante :

  • Un objet PHP possède un état
  • On vérifie l’état
  • On applique une transition
  • À la fin de la transition, l’état de l’objet PHP est modifié

Les transitions peuvent être plus ou moins complexes. C’est la différence entre une State Machine avec des transitions linéaires et un Workflow avec des transitions complexes. Le composant workflow propose bien entendu les deux possibilités.

Le problème

Les concepteurs·rices du framework Symfony ont fait un choix : celui de diriger la conception du code depuis la base de données. C’est pour cette raison que le code des entités se base sur des accesseurs/mutateurs ─ les getter/setter. Pour une propriété foo, un accesseur getFoo() ainsi qu’un mutateur setFoo($foo) vous seront proposés.

Le composant workflow, via son MarkingStore suit ce principe. Mon problème est donc le suivant : mon code ne respectant pas cette approche (au bénéfice d’une autre approche orienté métier), je n’utilise pas du tout ces mutateurs. Dans mon cas, une même opération agit sur une ou plusieurs propriétés et ces opérations sont explicitement nommées.

J’utilise souvent mes propres StateMachines dans ces projets métiers (alors que j’apprécie énormément ce composant dans des projets plus classiques) et ça m’embête. Ça m’embête d’avoir ces StateMachines à maintenir, ça m’embête d’avoir une StateMachines « maison » à côté d’un workflow etc.

La résolution

N'étant pas fan de cette situation, je me suis demandé s’il était possible de faire quelque chose et autant commencer par demander. Grégoire m'a alors rappelé qu'il est possible de créer son propre MarkingStore pour ensuite le déclarer dans la configuration du workflow.

Cela tombe très bien, car :

  • Symfony est un framework événementiel, le composant workflow propose lui aussi énormément d’événements
  • Entre la première version du composant et la version actuelle, une notion de contexte a été ajoutée. Cela permet d’ajouter des informations au MarkingStore.

J’ai donc opté pour la résolution suivante :

  • Créer un MarkingStore identique à celui par défaut et modifier le code afin de me servir du contexte ;
  • Me baser sur l’un des événements du composant, le TransitionEvent, pour récupérer des informations sur la transition ;
  • Me baser sur le nom des transitions, suffisamment explicite et correspondant le mieux aux méthodes disponibles dans l’entité.

Naïvement, on obtient les deux classes suivantes (copier-coller pour vous éviter de chercher et comprendre rapidement ce que fait le MarkingStore) :

<?php

namespace App\Infrastructure\Workflow;

use Symfony\Component\Workflow\Exception\LogicException;
use Symfony\Component\Workflow\Marking;
use Symfony\Component\Workflow\MarkingStore\MarkingStoreInterface;

final class AppMarkingStore implements MarkingStoreInterface
{
    private bool $singleState;
    private string $property;

    public function __construct(bool $singleState = true, string $property = 'state')
    {
        $this->singleState = $singleState;
        $this->property = $property;
    }

    public function getMarking(object $subject): Marking
    {
        $method = 'get'.ucfirst($this->property);
        if (!method_exists($subject, $method)) {
            throw new LogicException(sprintf('The method "%s::%s()" does not exist.', \get_class($subject), $method));
        }

        $marking = $subject->{$method}();
        if (!$marking) {
            return new Marking();
        }

        if ($this->singleState) {
            $marking = [(string) $marking => 1];
        }

        return new Marking($marking);
    }

    public function setMarking(object $subject, Marking $marking, array $context = []): void
    {
        $marking = $marking->getPlaces();

        if ($this->singleState) {
            $marking = key($marking);
        }

        $method = 'set'.ucfirst($this->property);

        // Le plus important
        if (true === isset($context['method'])) {
            if (true === method_exists($subject, $context['method'])) {
                $method = $context['method'];
            }
        }

        if (!method_exists($subject, $method)) {
            throw new LogicException(sprintf('The method "%s::%s()" does not exist.', \get_class($subject), $method));
        }

        $subject->{$method}($marking, $context);
    }
}
<?php
declare(strict_types=1);

namespace App\Infrastructure\Workflow;

use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Workflow\Event\TransitionEvent;

final class TransitionEventSubscriber implements EventSubscriberInterface
{
    public function onWorkflowTransition(TransitionEvent $event)
    {
        $context = $event->getContext();

        // Si la transition s'appelle "register", le context indiquera au MarkingStore d'aller voir si la fonction register est disponible
        $context['method'] = $event->getTransition()->getName();

        $event->setContext($context);
    }

    public static function getSubscribedEvents()
    {
        return [
            TransitionEvent::class => 'onWorkflowTransition',
        ];
    }
}

Côté configuration, nous aurons par exemple :

framework:
    workflows:
        registration:
            type: 'state_machine'
            audit_trail:
                enabled: true
            marking_store:
                service: App\Infrastructure\Workflow\AppMarkingStore
            supports:
                - App\Domain\Entity\Account
            initial_marking: !php/const App\Domain\Enum\StateEnum::CREATING
            places:
                - !php/const App\Domain\Enum\StateEnum::CREATING
                - !php/const App\Domain\Enum\StateEnum::REGISTERED
                - !php/const App\Domain\Enum\StateEnum::KNOWN
                - !php/const App\Domain\Enum\StateEnum::AFFILIATED
            transitions:
                register:
                    from: !php/const App\Domain\Enum\StateEnum::CREATING
                    to: !php/const App\Domain\Enum\StateEnum::REGISTERED
                complete:
                    from: !php/const App\Domain\Enum\StateEnum::REGISTERED
                    to: !php/const App\Domain\Enum\StateEnum::KNOWN
                affiliate:
                    from: !php/const App\Domain\Enum\StateEnum::KNOWN
                    to: !php/const App\Domain\Enum\StateEnum::AFFILIATED

Bonus : piqûre de rappel sur l’utilisation des constantes PHP en yaml.

Pour l’utilisation, tout est identique, vous pouvez vous référer à la documentation officielle.

Conclusion

Une fois de plus avec Symfony et ses composants, beaucoup de choses sont possibles. Il suffit de chercher, de fouiller et d’essayer. Le fait est qu'il est très facile d'utiliser le composant workflow quel que soit son cas d’usage et c’est le point le plus important.

Au jour le jour, les développeurs juniors ne sont pas forcément dépaysés, ils n’ont même pas à s’intéresser plus que ça au code du MarkingStore et peuvent l’utiliser sans s'en préoccuper. Cela me convient parfaitement.