Simplifiez vos applications avec le composant Workflow de Symfony

Publié le 3 octobre 2019 par Benoit Delesalle | php - symfony

Cet article est publié sous licence CC BY-NC-SA

Mais dis-moi Jamy, c’est quoi un workflow ?

Un workflow, c’est tout simplement un ensemble de règles qui régit un processus de notre application. Prenons un exemple pour mieux comprendre.

Imaginons que notre application soit un site e-commerce et que nous devons gérer l’état d’une commande. Pour cet exemple, on peut se dire que les états possibles d’une commande sont les suivants :

  • created : lorsqu’un client ajoute des produits à son panier.
  • waiting_for_payment : lorsque le client a validé son panier mais qu’il n’a pas encore payé sa commande.
  • awaiting_delivery : lorsque le client a payé sa commande mais qu’elle n’a pas encore été prise en compte par la société de livraison.
  • in_delivering : lorsque la commande a été prise en charge par la société de livraison.
  • delivered : lorsque la commande est bien arrivée à destination.
  • canceled : lorsque le client a annulé sa commande ou a abandonné son panier.

Maintenant que nous avons nos états, nous devons nous demander comment passer de l’un à l’autre. Dans notre exemple, nous ne pouvons pas passer de l’état « waiting_for_payment » à l’état « in_delivering » sans avoir reçu le paiement en question. Nous allons donc devoir définir les règles de changement d’état. Notre workflow pourrait donc ressembler à ceci.

Maintenant imaginons ce workflow dans notre application. Pour chaque changement d’état, il va falloir vérifier si ce changement est possible ou non, cela risque de nous procurer des heures de développement à se tirer les cheveux. Si seulement il existait une solution pour nous simplifier la vie. Mais attendez ! Elle existe cette solution, c’est le composant Workflow de Symfony.

Le composant Workflow

Pour cet article, nous supposons que notre application Symfony est en version 4.3. Dans un premier temps, installons le composant avec composer.

$ composer install symfony/workflow

Symfony vous crée un fichier de configuration de base qui ressemble à ceci :

# config/packages/workflow.yaml

framework:
  workflows: null

C’est dans ce fichier de configuration que nous allons définir tous nos workflows, leurs états possibles et comment passer d’un état à un autre. Ajoutons donc notre workflow concernant nos commandes.

# config/packages/workflow.yaml

framework:
  workflows:
    order:
      type: 'state_machine'
      marking_store:
        type: 'method'
        property: 'state'
      supports:
        - App\Entity\Order

Arrêtons-nous sur cette configuration. Nous avons donc créé un workflow qui se nomme order et qui est de type state_machine. Il en existe deux types :

  • state_machine
  • workflow

En soi, un state_machine est un workflow avec quelques différences :

  • un objet dans un state_machine ne peut avoir qu’un seul état à la fois contrairement à un workflow.
  • le state_machine est dit cyclique, c’est-à-dire que notre objet peut revenir dans un état qu’il a déjà connu, ce que ne peut pas faire un workflow.

Cela sous-entend également qu’un objet dans un workflow doit avoir connu tous les états précédents avant de passer à un nouvel état alors que le state_machine doit être dans au moins un de ces états.

Ensuite, nous avons défini avec l’option marking_store la manière dont nous voulons stocker l’état actuel de notre objet. Ici, cela veut simplement dire que nous allons stocker l’état dans un attribut state de notre objet en utilisant une méthode setState.

Finalement, nous avons indiqué les objets qui suivent ce workflow avec l’option supports, ici il n’y a que notre entité App\Entity\Order.

Tout cela est bien beau, mais comment définir les états possibles ? C’est très simple, allons voir cela de ce pas.

# config/packages/workflow.yaml

framework:
  workflows:
    order:
      type: 'state_machine'
      marking_store:
        type: 'method'
        property: 'state'
      supports:
        - App\Entity\Order
      initial_marking: created
      places:
        - created
        - waiting_for_payment
        - awaiting_delivery
        - in_delivering
        - delivered
        - canceled

Dans places, nous avons listé tous les états que peut prendre notre commande et nous avons spécifié dans initial_marking l’état de départ de notre objet.

Il ne nous reste plus qu’à préciser nos transitions, c’est-à-dire les règles de mouvement entre chaque état.

# config/packages/workflow.yaml

framework:
  workflows:
    order:
      type: 'state_machine'
      marking_store:
        type: 'method'
        property: 'state'
      supports:
        - App\Entity\Order
      initial_marking: created
      places:
        - created
        - waiting_for_payment
        - awaiting_delivery
        - in_delivering
        - delivered
        - canceled
      transitions:
        add_product_to_cart:
          from: created
          to: created
        confirm_order:
          from: created
          to: waiting_for_payment
        pay_order:
          from: waiting_for_payment
          to: awaiting_delivery
        accept_delivery:
          from: awaiting_delivery
          to: in_delivering
        delivery_done:
          from: in_delivering
          to: delivered
        cancel_order:
          from: [created, waiting_for_payment]
          to: canceled

Avec ces règles, il me sera désormais impossible de passer à un état in_delivering sans passer par la case paiement.

Vous pouvez désormais utiliser votre workflow dans votre application en injectant directement le Registry du composant Workflow dans votre constructeur et en récupérant votre workflow par son nom en lui passant l’objet sur lequel vous souhaitez l’appliquer.

<?php

// src/Controller/OrderController.php

namespace App\Controller;

use App\Entity\Commande;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Workflow\Registry;

class OrderController extends AbstractController
{
    /**
     * @var Registry
     */
    private $workflows;

    public function __construct(Registry $workflows)
    {
        $this->workflows = $workflows;
    }

    /**
     * @Route("/order", name="order")
     */
    public function order()
    {
        $order = new Order();
        $workflow = $this->workflows->get($order, 'order');
    }
}

Grâce à ce workflow, vous pouvez désormais vérifier simplement si votre commande peut passer à un état particulier et lui appliquer cet état en procédant comme ceci :

<?php

    if($workflow->can($order, 'pay_order')) {
        // votre code

        $workflow->apply($order, 'pay_order');
    }

Vous pouvez également utiliser ces conditions dans vos templates twig.


    <a href="...">Payer la commande</a>

Voilà, vous savez désormais comment vous faciliter la vie pour vos workflows.

Dans un prochain article, nous verrons comment aller plus loin en utilisant les événements liés aux workflows pour vraiment profiter de la puissance de ce composant.

Ressources

L’équipe Synbioz.
Libres d’être ensemble.