Auteur : Mamadou MASSI ABDOULAYE

Symfony Live - Le composant workflow

Le composant workflow de Symfony

Le talk « composant workflow » a été présenté par Hamza Amrouche.

Le composant workflow, est sorti avec Symfony 3.2. Il fournit des outils permettant de modéliser des processus ou des cycles de vie. Il permet de simplifier la gestion des graphs, les processus de validation et/ou machines à états, grâce à la programmation orientée objet. Un workflow est un processus ou un cycle de vie que vos objets traversent. Chaque étape du processus est appelée « place ». Vous définissez également des transitions qui décrivent l’action à effectuer d’une place à une autre. En pratique, pour créer un workflow, vous définissez des “états” et des “transitions” (qui sont les événements qui peuvent se produire entre deux états).

Pour rendre vos workflows plus flexibles, vous pouvez créer l’objet Workflow avec un EventDispatcher. Vous pouvez désormais créer des écouteurs d’événement pour bloquer les transitions (c’est-à-dire en fonction des données de l’object) et effectuer des actions supplémentaires lorsqu’une opération se produit.

1. Les nouveautés

1.1 L’exception TransitionException

// ...
use Symfony\Component\Workflow\Registry;
use App\Entity\BlogPost;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\Workflow\Exception\TransitionException;

class BlogController extends Controller
{
    public function edit(Registry $workflows)
    {
        $post = new BlogPost();
        $workflow = $workflows->get($post);

        // if there are multiple workflows for the same class,
        // pass the workflow name as the second argument
        // $workflow = $workflows->get($post, 'blog_publishing');

        $workflow->can($post, 'publish'); // False
        $workflow->can($post, 'to_review'); // True

        // Update the currentState on the post
        try {
            $workflow->apply($post, 'to_review');
        } catch (TransitionException $exception) {
            // ... if the transition is not allowed
        }

        // See all the available transitions for the post in the current state
        $transitions = $workflow->getEnabledTransitions($post);
    }
}

L’exception TransitionException a été introduite dans Symfony 4.1

1.2 La méthode addWorkflow()

Lorsque vous définissez plusieurs wokflow, vous devez envisager d’utiliser un registre, qui est un objet qui stocke et donne accès à différents workflow. Un registre aide également à décider si un workflow prend en charge l’objet utilisé:

// ...
use Symfony\Component\Workflow\Registry;
use Symfony\Component\Workflow\WorkflowInterface\InstanceOfSupportStrategy;
use Acme\Entity\BlogPost;
use Acme\Entity\Newsletter;

$blogWorkflow = ...
$newsletterWorkflow = ...

$registry = new Registry();
$registry->addWorkflow($blogWorkflow, new InstanceOfSupportStrategy(BlogPost::class));
$registry->addWorkflow($newsletterWorkflow, new InstanceOfSupportStrategy(Newsletter::class));
   

La méthode addWorkflow() a été introduite dans Symfony 4.1. Dans les versions précédentes de Symfony, il s’appelait add ().

2. Les améliorations

2.1 Injecter des metadata : cas d’utilisation

2.1.1 Cas d’utilisation

J’ai besoin d’injecter une route pour y avoir accès dans :

  • un workflow
  • une place
  • une transition

2.1.1.a Injecter des metadata : la configuration de mon workflow

# app/config/config.yml
workflows:
article_publishing:
    type: 'workflow' # or 'state_machine'
    marking_store:
        type: 'multiple_state' # or 'single_state'
        arguments:
            - 'currentPlace'
    supports:
        - AppBundle\Entity\Article
    places:
        - draft
        - review
        - rejected
        - published
    transitions:
        to_review:
            from: draft
            to:   review
        publish:
            from: review
            to:   published
        reject:
            from: review
            to:   rejected
class Article
{
    //la propriété qui stockera l'état
    public $currentPlace;

    public $title;

    public $content;
}

Notre exemple se base sur un article. Un article peut avoir l’un des statuts prédéfinis dans la config :

  • draft => status brouillon
  • review => status révisé
  • rejected => status rejeté
  • published => status publié

2.1.1.b Injecter des metadata : Récupérer les metadata (php)

$metadataStore = $workflow->getMetadataStore();

//framework.workflows.my_workflows.metadata.route
$metadataStore->getWorkflowMetadata()->get('route');

//framework.workflows.my_workflows.places.some_place.metadata.route
$metadataStore->getPlaceMetadata($place)->get('route');

//framework.workflows.my_workflows.places.some_transitions.metadata.route
$metadataStore->getTransitionMetadata($transition)->get('route');

2.1.1.c Injecter des metadata : Récupérer les metadata (twig)

$metadataStore = $workflow->getMetadataStore();

//framework.workflows.my_workflows.metadata.route
{{ workflow_metadata(article, 'route') }}

//framework.workflows.my_workflows.places.some_place.metadata.route
{{ workflow_metadata(article, 'route', place) }}

//framework.workflows.my_workflows.places.some_transitions.metadata.route
{{ workflow_metadata(article, 'route', transition) }}

2.1.2 Transition blocker ou expliquer pourquoi une transition est bloquée

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

class BlogPostReviewListener implements EventSubscriberInterface
{
    public function guardReview(GuardEvent $event)
    {
        /** @var \App\Entity\BlogPost $post */
        $post = $event->getSubject();
        $title = $post->title;

        if (empty($title)) {
           $event->addTransitionBlocker(
            new TransitionBlocker(
            "Impossible de publier cet article, il n'a pas de titre."
            ));
        }
    }

    public static function getSubscribedEvents()
    {
        return array(
            'workflow.blogpost.guard.to_review' => array('guardReview'),
        );
    }
}

2.1.2.a Pourquoi vous ne pouvez pas publier cet article?

<ul>
   {% for transition in workflow_all_transitions(article) %}
        {% if not workflow_can(article, transition.name) %}
            <ul>
                {% for blocker in framework.workflows_build_transition_blocker_list(article, transition.name) %}
                <li>
                    {{ blocker.message }}
                </li>
                {% endfor %}
            </ul>
        {% endif %
  {% endfor %}
</ul>

Source: