Le composant workfow

Ce composant, introduit dans Symfony 3.2, est extrêmement puissant. Il permet de créer des state machines (donc un seul état possible à chaque fois) ou des workflows (plusieurs états possibles à chaque fois). Cela peut être très utile lorsque vous désirez gérer une suite d’états dépendant d’un ou plusieurs autres états avec des règles précises de passage d’un état à un autre.

Il a été utilisé dans mon entreprise pour gérer le traitement de fichiers zip: dézippage, parcours de son contenu, lecture du xml contenu dedans, enregistrement en bdd…

Il est possible de s’en servir pour gérer des règles de gestion d’articles selon les rôles admin/journaliste, comme le montre le code de ce repo: https://github.com/lyrixx/SFLive-Paris2016-Workflow

Mots clef à retenir:

  • Transition: passage d’un état à un autre, représenté par des flèches ou des rectangles
  • Places: état, représenté par un rond

 

Les principales classes du composant workflow sont:

  • Transition: représente une Transition
  • Definition: contient les Places (string), les Transitions et l’état initial
  • Marking: contient l’état actuel / les états actuels (il s’agit d’un array de type [‘accepted’ => 1])
  • MarkingStore: est l’interface entre le workflow et le sujet (= l’objet)
  • Workflow: nous aide à décider quelles actions sont autorisées pour un sujet
  • Registry: stocke et fournit un accès aux différents workflows

 

Workflow vs Statemachine

La classe StateMachine étend la classe Workflow.

Dans une stateMachine, il n’y a toujours qu’un et un seul état possible:

Ici, soit on passe de in_review à changed_needed, soit on passe de in_review à published, mais on ne peut à la fois être dans l’état changes_needed et dans l’état published.

Dans un workflow, plusieurs états en parallèle sont possibles:

Ici, selon les transitions appliquées, on est dans un ou deux états parallèles:

  • wait_for_journalist & wait_for_spellchecker,
  • approved_by_journalist & wait_for_spellchecker,
  • ou published.

Et détail croustillant, on ne peut passer à l’état published que si les deux états approved_by_journalist et approved_by_spellchecker sont atteints. Vous commencez à voir l’intérêt du composant workflow autre que juste s’assurer que des étapes s’exécutent dans le bon ordre?

Définissons notre workflow

Tout d’abord, nous définissons un workflow de type stateMachine:

post: il s’agit du nom du workflow. Il aura son importance pour attraper les events dispatchés par le composant.
type: state_machine ou workflow?
audit_trail: permet de logger l’entrée et la sortie d’une place, l’exécution d’une transition.
support: l’entité pour laquelle nous allons suivre l’évolution du statut
marking_store.type: est-ce de type single_state ou multiple_state?
marking_store.arguments: par défaut, cette valeur est marking, ce qui oblige à définir un attribut marking dans l’entité. Ici, nous décidons que le statut aura la propriété status en base et dans notre entité
places: les états possibles que notre entité va avoir
transitions: les transitions pour passer d’un état à un autre

Rajoutons l’attribut $status à notre entité

 

Les filtres Twig

Twig possède quatre filtres qui permettent d’interagir avec le composant workflow:

  • workflow_can()Retourne true si l’objet donné peut faire la transition
  • workflow_transitions()Retourne un tableau avec toutes les transitions possibles pour l’objet
  • workflow_marked_places()Retourne un tableau avec les états courants de l’objet
  • workflow_has_marked_place()Retourne true si l’objet a bien l’état donné
 

Dans le cas d’un crud, nous pouvons mettre ceci dans la vue:

Au départ, nous avons le bouton Accept:

En cliquant dessus, nous voyons apparaître les boutons reject et publish:

 

En cliquant sur publish, nous voyons apparaître le bouton unpublish

L’action du contrôleur pour appliquer les transitions

Mais pour que le comportement ci dessus fonctionne, il faut bien sûr appliquer la transition à chaque fois dans le contrôleur:

Et c’est tout! A chaque fois que l’on appliquera une transition, le nom du nouveau statut sera enregistré en base sous forme de string.

Les events

Ah mais ce n’est pas tout. S’il ne permettait que faire cela, le composant workflow serait un peu limité. Mais à chaque étape, de nombreux events sont dispatchés!

workflow.guard

Valide si une transition est autorisée

Les trois events dispatchés sont:

  • workflow.guard
  • workflow.[workflow name].guard
  • workflow.[workflow name].guard.[transition name]

workflow.leave

L’objet est sur le point de quitter un état (une place)

Les trois events dispatchés sont:

  • workflow.leave
  • workflow.[workflow name].leave
  • workflow.[workflow name].leave.[place name]

workflow.transition

L’objet est en train d’effectuer sa transition

Les trois events dispatchés sont:

  • workflow.transition
  • workflow.[workflow name].transition
  • workflow.[workflow name].transition.[transition name]

workflow.enter

L’objet est entrée dans un nouvel état (place). C’est le premier event où l’objet est marqué comme étant à une nouvelle place

Les trois events dispatchés sont:

  • workflow.enter
  • workflow.[workflow name].enter
  • workflow.[workflow name].enter.[place name]

workflow.entered

Similaire à workflow.enter, excepté que le champ status (le marking store) est mis à jour avant cette event (en faisant une bonne place pour flusher les data dans Doctrine).

Les trois events dispatchés sont:

  • workflow.entered
  • workflow.[workflow name].entered
  • workflow.[workflow name].entered.[place name]

workflow.announce

Lancé à chaque transition qui est à présent accessible pour l’objet

Les trois events dispatchés sont:

  • workflow.announce
  • workflow.[workflow name].announce
  • workflow.[workflow name].announce.[transition name]

Notre premier event

Créons un subscriber qui va écouter la transition publish et mettre à jour la date de publication:

 

N’oublions pas de l’enregistrer dans la classe services.yml:

Il n’y a rien d’autre à faire.

Et si je veux ne permettre qu’aux super admins de publier ou de dépublier?

Très bonne question, et la réponse est extrêmement simple:

1) soit en écoutant les events workflow.post.guard.publish et workflow.post.guard.unpublish en injectant l’authorization_checker et en appelant la méthode isGranted

 

2) soit en faisant beaucoup plus simple que cela avec l’expression language:

Elle est pas belle, la vie ? 😉

Notre propre service de mise à jour de statut

Eh oui, jusque là, nous avons laissé le composant workflow mettre à jour le statut, mais si nous décidions qu’il devait être mis à jour selon des règles précises?

Nous allons créer un service permettant de mettre à jour le statut sous forme de bit (pour permettre le cumul des droits). Ainsi, IS_NEW correspondra au bit 1 (0001), ACCEPTED au bit 2 (0010), et PUBLISHED au bit 4 (0100).

 

Déclarons le service:

 

Modifions le workflow:

 

Et modifions notre entité:

Bon, là, je vous vois venir, le fait que j’ai utilisé des bits ne sert pas à grand chose et vous avez raison. En fait, c’est par prévoyance. C’est à dire que si je créé à un moment le statut 3, que va-t-il se passer? Bingo! J’aurai comme places possibles celle du statut 1 et celle du statut 2 car 0011, c’est l’addition de 0001 et 0010.

Modification de la requête de récupération des articles

Nous ne voulons récupérer que les articles qui sont publiés, donc qui ont le champ status avec un bit 4. Pour vérifier cela, nous allons faire appel à la fonction BIT_AND qui fait un & et qui regarde ce qu’il y a de commun entre deux nombres exprimés sous forme de bits.

Par exemple, 6 & 3 donnera 2: 0110 & 0011.
Ce qu’il y a en commun est 0010, soit 2.

Nous allons comparer le statut de chaque article avec 4 pour vérifier que le & donne un nombre supérieur à 0.

4 & 1 donne 0.
4 & 2 donne 0.
4 & 4 donne 4.

La requête devient donc:

 

Le mot de la fin

Ce composant est vraiment puissant et regorge de possibilités.

Attention, il y a quelques spécificités:

  • Si on utilise un service pour gérer le statut, on ne peut plus mettre type: single_state ou type: multiple_state dans la config (c’est pour ça que je l’ai commenté)
  • Si on utilise un type worflow plutôt qu’un type state_machine, il faudra appeler le service $this->get(‘workflow.nom_du_workflow’) plutôt que $this->get(‘state_machine.nom_du_workflow’)
  • Il est bien sûr possible, dans un workflow, de définir plusieurs places dans un from du style:
     

Rédigé par

2 comments

  1. Encore merci Jean Pierre pour cette présentation de l’outils workflow.
    C’est exactement ce qu’il me faut pour un projet en cours.

    Bonne continuation

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *