Aller au contenu
AFUP AFUP Day 2025 Baromètre Planète PHP PUFA
 

A la decouverte du Workflow

Description

Il est courant de gérer des statuts ou états de produits, dans des sites e-commerce ou éditoriaux. Pour ce faire, il est possible d'agir de façon classique, c'est-à-dire manuellement, ou d'avoir recours à des librairies qui proposent déjà des bases de code. Pour cette présentation, nous aborderons la notion de machine à état et sa définition avant d'envisager, son application, en examinant les librairies open sources existantes. Enfin, à la lumière de Symfony, nous étudierons le nouveau composant "Workflow"

Conférence donnée lors du PHP Tour Montpellier 2018, ayant eu lieu les 17 et 18 mai 2018.

Informations complémentaires

Vidéo

Le speaker

Gregoire PINEAU

Arrivé en 2017 dans l’équipe de JoliCode, Grégoire a toujours aimé bidouiller, comprendre et apprendre. À l’issue d’études éclectiques, il est revenu au Web en 2010, domaine dans lequel il exerce depuis avec passion. Après avoir appris à se servir du framework Symfony, il a passé sa certification, puis a commencé à contribuer timidement… Ce qui l’a mené, quelques années après, à devenir un des core contributeurs du projet?! Durant toutes ces années, il a toujours préféré le backend au frontend – même s’il apprécie React, Sass et ces autres joyeusetés, il s’amuse davantage avec Ansible, AWS ou Consul. Vous pourrez le croiser lors de meetups, ou dans des matches de Volley :-)

Commentaires

Très intéressant
Ulrich, le 23/05/2018
Pour moi ça manquait d'une intro au début, Gregoire a directement commencer a parler des composants du workflow sans vraiment dire qu'il allait présenté en détails les différent composants et non faire un simple tour d'horizon du composant.
Benjamin Lévêque, le 25/05/2018

Transcript

Bonjour. On est dans une salle de cinéma j'étais obligé de le faire ça ! Comme on m'a présenté, je vais passer très rapidement : vous pouvez me retrouver sur Github, Twitter, etc. sous le pseudo @lyrixx Aujourd'hui on va parler de Workflow. La première question : quand est-ce qu'on utilise un Workflow ? Imaginez que vous travaillez dans une entreprise de presse et vous voulez publier des articles sur l'actualité.

Vous pouvez pas directement écrire un article et le publier : on va avoir des étapes de validation, on veut que l'article soit bien écrit, on va vérifier les fautes d'orthographe, le sourcing, etc.

Donc on va définir un Workflow précis avant de pouvoir mettre l'article en ligne.

Chacune de ces étapes est nécessaire, on ne peut pas écrire un brouillon et directement le publier en ligne.

Bon certains sites de presse font ça, mais c'est pas vraiment des très bons sites.

Ce circuit de production et de publication est une analogie de ce qu'on retrouve dans le code.

Pourquoi un composant ? Je vous demande pas de lire tout ce code, c'est une gestion de statuts pour une commande.

Le code est compliqué, on l'alourdit dès qu'on veut ajouter un état, donc il sera de plus en plus compliqué à maintenir.

Le but c'est d'éviter d'avoir ce genre de code à maintenir.

Avoir un composant de Workflow permet de découpler le code vraiment spécifique aux états, aux transitions, etc.

du code qui est dans votre entité, dans votre modèle.

Le composant Workflow va vérifier et valider qu'on passe bien par toutes les étapes, avant de mettre son article en prod.

Donc, Symfony Workflow : il y a plusieurs façons de l'utiliser.

La première, c'est la machine a état.

On a un état A, à gauche, une transition, T1, et un état B.

Donc je peux être dans l'état A, appliquer la transition T1 et me retrouver dans l'état B.

Par exemple j'ai un état brouillon et j'ai un état en production C'est un graphe orienté. Par exemple, j'ai l'état A, je vais passer à l'état B, et là je dois faire un choix : prendre la transition T2 et arriver à l'état C en haut, ou prendre la transition T3 et arriver à l'état D en bas.

Admettons que je prenne la transition T3 pour arriver dans l'étape D, je vais ensuite prendre la transition T5 pour arriver dans l'état E.

C'est un graphe orienté : je peux aller dans un seul sens, la flèche est de gauche à droite.

Si je suis dans l'état E, par exemple, je ne peux pas revenir à l'état C ni à l'état D.

Par contre il aurait pu exister une transition T6 qui revient en arrière, mais tant qu'il n'y a pas de transition, je ne peux pas revenir en arrière.

Pour décrire ça dans Symfony, dans le composant, on a une classe de définition.

Prenons cet exemple : je peux aller de A à B, de B à C et depuis C, je peux revenir à a grâce à la transition T3.

Comment ça marche ? J'ai des places A, B, C (des strings) et des transitions (des objets).

Je vais appeler ma première transition "T1", le deuxième argument c'est l'endroit d'où je viens, et le troisième c'est l'endroit où je vais.

Et je vais créer toutes mes transitions. Il y a une petite faute dans la slide, c'était "T1", "T2", "T3".

Déjà une fois qu'on a fait ça, on a très peu de code. Et on a un truc super pratique : GraphViz Dumper, qui me sert à faire les schémas dans les slides.

J'ai juste à créer ma définition, je lui donne les places et les transitions, et grâce au GraphViz Dumper, je peux dumper cette représentation.

GraphViz c'est un format pour grapher des choses (des graphes en général), et après, j'ai juste à exécuter par exemple PHP test et sous Linux il y a un outil : GraphViz, le binaire s'appelle "dot" donc dot.Tpng et j'ai mon image.

C'est super pratique pour vérifier que son Workflow ressemble à ce qu'on avait en tête.

La classe Workflow : il y a quelques méthodes dessus.

La méthode can() prend un sujet avec n'importe quel type d'objet en entrée, et une transition.

Donc ça sert à vérifier que je peux faire cette transition.

Imaginons que je suis dans l'état B, je vais demander si je peux prendre la transition T2 pour aller dans l'état C.

La fonction va dire oui ou non. En l'occurrence, je suis dans l'état B, j'ai le droit de prendre la transition T2.

Si je lui demande par exemple pour T3, je ne pourrai pas la prendre parce que je n'étais pas dans l'état C.

La méthode apply() prend aussi le sujet en entrée et le nom de la transition.

Là si j'étais en B, je fais apply() avec T2 je vais me me retrouver automatiquement en C.

La méthode getEnabledTransitions() retourne toutes les transitions possibles.

Imaginez que dans votre backoffice vous voulez mettre des boutons pour savoir si vous avez le droit de mettre en prod, de faire ci ou ça, cette méthode vous permet de retrouver toutes les transitions possibles.

Là par exemple si je suis en C, la méthode va retourner seulement T3.

Dans Twig vous retrouvez à peu près la même chose, il y a une intégration.

"Est-ce que j'ai le droit de faire cette transition ?" (par exemple pour mettre un lien, un bouton), ou alors "Donne moi toutes les transitions disponibles pour cet objet là" (à chaque fois je dois passer un objet).

En plus de ça il y a des événements.

L'Event Dispatcher est dans la classe Workflow et vous aurez des événements dispatchés à plein de moments.

Donc dès que vous sortez d'une place, que vous passez à travers une transition ou que vous arrivez dans une nouvelle place, vous avez un événement.

Et en plus, dès qu'une transition devient possible, vous avez un nouvel événement.

Ce qui peut être très pratique par exemple si vous voulez faire des logs, savoir qui a fait quoi à quel moment.

Vous pouvez utiliser tous ces événements pour enrichir votre Workflow.

Le nom des événements c'est toujours "workflow." (Workflow point) et à chaque fois on a trois événements par type d'événement.

Un événement global, par exemple "workflow leave", dès qu'on sort d'une place, puis, si mon Workflow s'appelle "workflow_name", j'aurai "workflow.workflow_name.leave" et enfin, "workflow.workflow_name.leave.a" pour indiquer qu'on est sorti de la place a.

Donc ça c'est à chaque fois pour tous les événements.

En plus de tout ça, un dernier type d'événement dispatchés c'est le Guard Event.

On a vu qu'on pouvait créer un graphe et dire "je peux passer de A à B, de B à C, etc.".

Mais parfois vous voulez avoir plus de contraintes, comme des contraintes métier, par exemple : seul un journaliste peut mettre en prod, et pas un stagiaire.

Dans la sécurité, la personne d'avoir un rôle "journaliste", et vous voulez coder ça.

Mais vous ne pouvez pas le faire juste avec le Workflow, vous devez créer du PHP, écrire un peu de code, pour dire que seules ces personnes peuvent effectuer cette transition-là.

Vous allez pouvoir faire ça grâce au Guard Event.

L'utilisation est assez simple : vous pouvez demander si c'est bloqué, le bloquer, et depuis Symfony 4.1, il y a la fonction addTransitionBlocker() qui permet, en plus de bloquer l'événement, d'en donner la raison.

Ce qui est pratique pour l'utilisateur final, parce que dire "non tu peux pas faire ça" sans raison, c'est pas cool.

et depuis la version 4.2 de Symfony, on va pouvoir expliquer à l'utilisateur pourquoi est bloqué.

Grâce à cet événement, vous pouvez créer n'importe quel type de règle métier, pour bloquer quelque chose.

Là on a vu les machines à état, qui sont normalement assez connues, que beaucoup peuvent avoir déjà utilisé ou implémenté dans son projet Et on va parler de quelque chose d'un peu plus intéressant : les réseaux de Pétri.

C'est un peu comme une machine à état, mais sous stéroïdes, dans le sens où on a beaucoup plus de fonctionnalités.

Dans les règles de l'art, une machine à état est un subset des réseaux de Pétri.

Les réseaux de Pétri sont un peu plus compliqués, mais une machine à état est juste un réseau de Pétri spécial, donc si on peut faire un réseau de Pétri on peut faire une machine à état, et le composant Workflow de Symfony, c'est des réseaux de Pétri.

J'ai toujours une place A, une transition T1 et une place B, mais on a modélisé la transition.

Avant on avait juste un trait pour la transition, ici on a un carré : c'est modélisé.

Ce qui permet d'avoir plusieurs entrées et plusieurs sorties : différentes places convergent dans la transition et de la transitions ressortent plusieurs places.

Si on fait une petite démo : pour passer dans la transition, il me faut un token (un token c'est juste dire "je suis dans cette place-là").

Là j'ai deux tokens en amont de la transition et au moment où j'applique la transition, la transition prend tous les tokens des places en amont, les consomme et en met de nouveaux dans les places en sortie.

Le nombre de place en amont peut être totalement décorrélé du nombre de places en sortie, ce n'est pas forcément le même.

Les réseaux de Pétri ont été modélisés il y a longtemps pour gérer par exemple les chaînes de production.

Par exemple, chez Ikea, on va modéliser la fabrication d'une chaise : les places avant c'est 4 barreaux, un plateau, un dossier.

La transition, c'est la machine. La machine prend les 4 barreaux, le plateau, le dossier, consomme tout et fournit une chaise.

Donc en amont j'avais peut-être que 3 places et en aval, je n'en aurai qu'une.

Donc on dans plusieurs états en même temps (histoire de mettre un chat !) Quelques exemples de workflows : on peut avoir des choses très simples (je suis en A, par T1 je passe en B, etc.), on peut avoir des boucles comme tout à l'heure, on peut avoir des choses un peu plus compliquées (là c'est un choix à faire, je passe soit en haut via T1, j'arrive en B, soit en bas via T2, j'arrive en C, etc.) et là où ça devient super intéressant, c'est qu'on peut avoir des tâches en parallèle.

Là quand je suis en A, je passe par T1 et je me retrouve à la fois dans B et dans C, donc je vais pouvoir avancer soit d'abord via T2, soit d'abord via T3.

Donc je vais devoir jouer dans tous les cas T2 et T3 pour qu'à un moment donné je sois dans D et dans E.

Et seulement quand je serai dans ces deux états-là, j'aurai le droit de passer dans T4.

Parce que pour qu'une transition soit activée il faut qu'il y ait un token dans absolument toutes les places en amont de cette transition.

Ou on peut faire des trucs plus compliqués ça marche de la même façon.

Si on revient sur notre problématique initiale d'un article, on pourrait très bien avoir ça.

Mon article au début est dans un état "brouillon", je vais faire une demande de relecture, je vais être en attente d'un journaliste et d'un correcteur qui me relisent (les deux peuvent se jouer dans n'importe quel ordre) et ensuite par exemple le journaliste approuve et le correcteur approuve, et là enfin on peut publier et mettre en production.

Le marking : c'est un objet PHP qui contient comment est votre objet dans le graphe.

Il va juste contenir toutes les places de votre objet.

Les ronds rouges indiquent où je me trouve actuellement : donc ici, le marking contient "attente journaliste" et "correcteur OK".

Si j'applique la transition "journaliste approuve", mon marking sera différent et contiendra "journaliste OK" et "correcteur OK".

J'ai une petite démo (normalement ça marche !), elle est dispo sur internet, je vous donnerai le lien après.

C'est un peu moche mais ça montre toutes les fonctionnalités du composant Worflow d'un seul coup.

Par défaut en fait ici je peux rien faire, tous les boutons sont rouges, quand je peux faire quelque chose il sont bleus.

Je dois être connecté pour pouvoir faire quelque chose. Donc je me connecte en tant qu'Alice par exmple.

Alice n'a pas de rôle spécial, elle peut juste demander une review.

Elle a écrit son article, elle clique ici pour demander une review. Et maintenant elle ne peut plus rien faire.

Donc je me reconnecter avec quelqu'un d'autre, par exemple, Checker. Lui, il peut uniquement valider la correction orthographique.

Il n'y a pas de faute dans l'article, il valide, et pareil, il ne peut plus rien faire d'autre.

Bon, je vais me connecter directement en tant qu'admin : il va pouvoir valider.

Vous voyez le Workflow avance (les ronds rouges avancent au fur et à mesure). Et voilà l'article est publié ! Donc on a le workflow qui sait fonctionner, on a le marking qui représente où en est l'objet sur le graphe, et on a le marking store : c'est une interface entre le composant Workflow et votre objet.

Parce qu'en fait vous pouvez stocker n'importe comment l'état de votre objet.

Par défaut votre objet c'est juste une classe PHP, il n'y a rien de spécial à faire.

Donc il y a deux implémentations natives dans Symfony.

La première s'appelle Single State Marking Store : je suis dans un seul état possible, donc je ne suis pas sur un réseau de Pétri.

Je suis sur une machine à état classique, je peux être qu'à un seul endroit à la fois.

C'est le plus simple, parce que si vous utilisez une base de données, vous avez juste à définir une string (état A, état B, état C, etc.).

La deuxième c'est le Multiple State Marking Store, qui sera utile pour les réseaux de Pétri, parce que vous pourrez être dans plusieurs états à la fois, et pour stocker plusieurs états, vous devrez stocker un tableau dans une base de données par exemple.

Le tableau c'est une façon de faire, on pourrait aussi stocker un champs de bits, et retrouver quels sont les états grâce à un champs de bits, ça peut potentiellement être plus optimisé, etc.

Donc le Marking Store c'est l'interface entre le composant Workflow et votre classe PHP.

Les metadata sont quelque chose de nouveau dans Symfony pour la version 4.1, qui vous permettent d'attacher des données à votre Workflow : des métadonnées sur le Workflow lui-même, sur les places ou sur les transitions.

Par exemple vous pourrez donner des noms un peu plus sympathique à l'utilisateur, plutôt que le nom que vous utilisez directement dans le code, vous pourrez traduire des choses, et tout plein d'informations.

Donc là on a vu le composant Workflow en dehors du framework full-stack de Symfony.

On va voir maintenant comment on l'utilise dans Symfony avec le framework banal.

Donc dans le fichier config.yaml, vous aurez une entrée qui va s'appeler workflows, et là vous donnez le nom de votre Workflow (par exemple "article"), le type donc soit une stack machine soit un workflow (ou réseau de Pétri), le type d'objets qu'il supporte, les places, les transitions et les metadata.

Si on détaille, ça ressemble à ça : j'ai une place qui s'appelle "draft", une metadata "title", etc.

Donc j'ai toutes mes places, j'ai aussi toutes mes transitions (ici par exemple "request review"), et là j'ai aussi le guard - donc sans coder de PHP je peux directement ici dire qu'il faut être connecté pour exécuter cette transition.

Là, "from", je peux venir de du draft et "to" ici j'ai deux états, donc je suis forcément dans un réseau de Pétri (ou un Workflow), pas dans une machine à état.

Et j'ai des métadonnées, sur la deuxième transition, en-dessous, guard, vous voyez qu'on peut faire un "is granted role journalist", comme vous faites dans Symfony avec des controllers @security par exemple, ou "this security -> Is_Granted" c'est la même chose.

En plus de ça, on a une feature depuis Symfony 3.3 ou 3.4 : audit_trail.

Quand vous activez l'audit trail, tout ce qui se passe dans le Workflow est loggé, ça peut être pratique en cas de débug ou en prod pour savoir ce qui se passe.

On a aussi une commande qui nous permet de dumper, pour voir graphiquement à quoi ressemble notre Workflow.

C'est la même chose que ce qu'on a vu au début de la conférence, sauf que là c'est pluggé directement avec Symfony, tout est fait pour.

Dans une vue, là vous êtes dans le Twig, et on a vraiment tout le truc au complet : je vais créer un formulaire, qui a une action (par exemple 'article_apply_transition'), donner l'id de mon article et c'est en post.

Je fais un "for transition in workflow_transition()" donc je récupère toutes les transitions possibles, et je vais mettre un bouton.

J'ai volontairement simplifié le code, ça aurait été bien de mettre une protection CSRF.

Et dans le contrôleur (c'est un peu gros mais il y a tout, c'est le seul code que vous avez besoin de faire) : je récupère mon article, je récupère le nom de la transition et j'essaie de l'exécuter directement.

Je mets quand-même dans un try/catch, je vais vous expliquer pourquoi en suivant.

Si ça marche, c'est cool je pourrai récupérer par exemple le nom de la transition, dans les FlashBag, je vais ajouter "vous avez fait cette transition-là" et je vais sauvegarder.

Qu'est-ce qu'il peut y avoir comme problème ? Par exemple, vous avez deux onglets ouverts, vous avez effectué la transition dans le premier onglet, du coup l'objet a déjà avancé dans le graphe et dans le deuxième onglet c'est pas possible.

La transition ne sera plus valide. Donc vous aurez une exception et dans ce cas, vous affichez le message à l'utilisateur.

Donc juste avec ses deux choses, plus la configuration, on manage un objet.

C'est fini ! On a quand-même des petits trucs en plus. Par exemple, là on a un guard (c'est un autre exemple, on est plus sur les articles), par exemple sur des tâches, je veux pouvoir valider une tâche seulement de telle heure à telle heure.

Du coup j'ai juste à implémenter une classe EventSubscriberInterface et une fois que j'ai implémenté la classe (avec Symfony 4 s'est autowired, auto-configuré donc j'ai juste à créer ce fichier-là), et je m'abonne tout en bas à Workflow, workflow.task.guard.done, donc dès qu'on essaye de passer dans la transition "done", je vais exécuter ce bout de code là, qui récupère les metadata que j'avais mis dans la configuration, si jamais il est moins de telle heure ou plus de telle heure, je vais bloquer la transition, elle ne sera pas exécutable.

Voilà ! Un petit peu rapide... Merci Est-ce que vous avez des questions ? Merci beaucoup.

Ma question était au sujet de séparer la logique métier dans des classes à part, donc de ne pas permettre au modèle de se valider lui-même.

Qu'est ce que tu en penses ? Honnêtement je n'en pense rien ! Il y a vraiment deux écoles : certains préfèrent tout mettre dans le modèle, grand bien leur fasse.

Moi, à la longue... dans le bout de code que je montrais au début, il n'y avait que 3 ou 4 états.

Quand tu vas en rajouter un cinquième, un sixième, un septième, ton code va exploser, donc à moins d'avoir un trait ou que tu recodes en gros la state machine...

Parce que tu peux pas faire tous les cas à la main, c'est pas possible tu vas forcément te planter, donc il faut un truc un peu industrialisé qui dit "voilà mes états, mes transitions, ce qui est possible".

Perso, je préfère le composant Workflow, mais je comprends les gens qui préfèrent que tout soit dans leur modèle.

C'est juste une question de maintenance je pense... je veux fâcher personne ! D'autres questions ? J'en profite pour proposer une question : est-ce qu'il existe un moyen simple d'enregistrer tous les états par lequel est passé un objet ? Un peu comme ce qu'on pourrait retrouver avec de l'event sourcing, où la source de vérité c'est tout tous les événements associés à un objet ? Oui, grâce aux événements qui vont être déclenchés, tu peux savoir qui a fait quoi.

Tu t'abonnes aux événements Symfony, et dans une base de données ou autre, tu vas voir que telle personne a déclenché telle transition, qu'on était dans tel état avant, qu'on est dans tel état maintenant. Tu peux tout faire grâce aux événements.

Ok merci.

Pas d'autres questions ? Merci pour la présentation, c'était vachement intéressant.

Pour le stockage, le Marking Store, apparemment, il n'y a rien de prévu pour faire autre chose que stocker dans l'entité ? En fait tu peux faire vraiment ce que tu veux. Tu as ton objet, admettons un article, donc par défaut, à un moment la méthode can, méthode apply, va utiliser cet article là, et le Workflow va faire demander au Marking Store associé : "Donne-moi le marking de ce truc-là".

Donc ton marking, tu pourrais très bien le stocker dans une API, rien à voir avec ton objet en fait.

C'est découplé en fait ? On peut faire le Marking Store qui stocke où on a envie.

Par mesure de simplicité dans Symfony, ça y est, du coup tu utilises l'un des deux Marking Store implémentés, et eux viennent faire des Get et des Set dans ton entité, mais ça c'est juste par défaut.

Tu as une interface qui n'a vraiment que deux méthodes : getMarking() et setMarking() qui reçoit ton objet, donc après tu stockes ça vraiment où tu veux, c'est totalement découplé.

Merci.