Workflow

    In applications using Symfony Flex, run this command toinstall the workflow feature before using it:

    Configuration

    To see all configuration options, if you are using the component inside aSymfony project run this command:

    1. $ php bin/console config:dump-reference framework workflows

    A workflow is a process or a lifecycle that your objects go through. Eachstep or stage in the process is called a place. You do also define _transitions_to that describes the action to get from one place to another.

    A set of places and transitions creates a definition. A workflow needsa and a way to write the states to the objects (i.e. aninstance of a MarkingStoreInterface.)

    Consider the following example for a blog post. A post can have these places:draft, reviewed, rejected, published. You can define the workflowlike this:

    • YAML
    1. # config/framework.yaml
    2. framework:
    3. workflows:
    4. blog_publishing:
    5. type: 'workflow' # or 'state_machine'
    6. audit_trail:
    7. enabled: true
    8. marking_store:
    9. type: 'method'
    10. property: 'currentPlace'
    11. supports:
    12. - App\Entity\BlogPost
    13. initial_marking: draft
    14. places:
    15. - draft
    16. - reviewed
    17. - rejected
    18. - published
    19. transitions:
    20. to_review:
    21. from: draft
    22. to: reviewed
    23. publish:
    24. from: reviewed
    25. to: published
    26. reject:
    27. from: reviewed
    28. to: rejected
    • XML
    1. <!-- app/config/config.xml -->
    2. <?xml version="1.0" encoding="UTF-8" ?>
    3. <container xmlns="http://symfony.com/schema/dic/services"
    4. xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    5. xmlns:framework="http://symfony.com/schema/dic/symfony"
    6. xsi:schemaLocation="http://symfony.com/schema/dic/services https://symfony.com/schema/dic/services/services-1.0.xsd
    7. http://symfony.com/schema/dic/symfony https://symfony.com/schema/dic/symfony/symfony-1.0.xsd"
    8. >
    9.  
    10. <framework:config>
    11. <framework:workflow name="blog_publishing" type="workflow">
    12. <framework:audit-trail enabled="true"/>
    13. <framework:marking-store type="single_state">
    14. <framework:argument>currentPlace</framework:argument>
    15. </framework:marking-store>
    16. <framework:support>App\Entity\BlogPost</framework:support>
    17. <framework:initial-marking>draft</framework:initial-marking>
    18. <framework:place>draft</framework:place>
    19. <framework:place>reviewed</framework:place>
    20. <framework:place>rejected</framework:place>
    21. <framework:place>published</framework:place>
    22. <framework:transition name="to_review">
    23. <framework:from>draft</framework:from>
    24. <framework:to>reviewed</framework:to>
    25. </framework:transition>
    26. <framework:transition name="publish">
    27. <framework:from>reviewed</framework:from>
    28. <framework:to>published</framework:to>
    29. </framework:transition>
    30. <framework:transition name="reject">
    31. <framework:from>reviewed</framework:from>
    32. <framework:to>rejected</framework:to>
    33. </framework:transition>
    34. </framework:workflow>
    35. </framework:config>
    36. </container>
    • PHP
    1. // app/config/config.php
    2. $container->loadFromExtension('framework', [
    3. // ...
    4. 'workflows' => [
    5. 'blog_publishing' => [
    6. 'type' => 'workflow', // or 'state_machine'
    7. 'audit_trail' => [
    8. 'enabled' => true
    9. ],
    10. 'marking_store' => [
    11. 'type' => 'method'
    12. 'property' => ['currentPlace']
    13. ],
    14. 'supports' => ['App\Entity\BlogPost'],
    15. 'initial_marking' => 'draft',
    16. 'places' => [
    17. 'draft',
    18. 'reviewed',
    19. 'rejected',
    20. 'published',
    21. ],
    22. 'transitions' => [
    23. 'to_review' => [
    24. 'from' => 'draft',
    25. 'to' => 'reviewed',
    26. ],
    27. 'publish' => [
    28. 'from' => 'reviewed',
    29. 'to' => 'published',
    30. ],
    31. 'reject' => [
    32. 'from' => 'reviewed',
    33. 'to' => 'rejected',
    34. ],
    35. ],
    36. ],
    37. ],
    38. ]);

    Tip

    If you are creating your first workflows, consider using the workflow:dumpcommand to .

    As configured, the following property is used by the marking store:

    1. class BlogPost
    2. {
    3. // This property is used by the marking store
    4. public $currentPlace;
    5. public $title;
    6. public $content;
    7. }

    Note

    The marking store type could be "multiple_state" or "single_state". A singlestate marking store does not support a model being on multiple places at thesame time. This means a "workflow" must use a "multiple_state" marking storeand a "state_machine" must use a "single_state" marking store. Symfonyconfigures the marking store according to the "type" by default, so it'spreferable to not configure it.

    A single state marking store uses a string to store the data. A multiplestate marking store uses an array to store the data.

    Tip

    The marking_store.type (the default value depends on the type value)and arguments (default value ['marking']) attributes of themarking_store option are optional. If omitted, their default values willbe used. It's highly recommenced to use the default value.

    Tip

    Setting the audit_trail.enabled option to true makes the applicationgenerate detailed log messages for the workflow activity.

    Using Events

    To make your workflows more flexible, you can construct the Workflowobject with an EventDispatcher. You can now create event listeners toblock transitions (i.e. depending on the data in the blog post) and doadditional actions when a workflow operation happened (e.g. sendingannouncements).

    Each step has three events that are fired in order:

    • An event for every workflow;
    • An event for the workflow concerned;
    • An event for the workflow concerned with the specific transition or place name.When a state transition is initiated, the events are dispatched in the followingorder:

    • workflow.guard

    • Validate whether the transition is blocked or not (see andblocking transitions).

    The three events being dispatched are:

    • workflow.guard
    • workflow.[workflow name].guard
    • workflow.[workflow name].guard.[transition name]
      • workflow.leave
      • The subject is about to leave a place.

    The three events being dispatched are:

    • workflow.leave
    • workflow.[workflow name].leave
    • workflow.[workflow name].leave.[place name]
      • workflow.transition
      • The subject is going through this transition.

    The three events being dispatched are:

    • workflow.[workflow name].transition
    • workflow.[workflow name].transition.[transition name]
      • workflow.enter
      • The subject is about to enter a new place. This event is triggered justbefore the subject places are updated, which means that the marking of thesubject is not yet updated with the new places.

    The three events being dispatched are:

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

    The three events being dispatched are:

    • workflow.entered
    • workflow.[workflow name].entered
    • workflow.[workflow name].entered.[place name]
      • workflow.completed
      • The object has completed this transition.

    The three events being dispatched are:

    • workflow.completed
    • workflow.[workflow name].completed
    • workflow.[workflow name].completed.[transition name]
      • workflow.announce
      • Triggered for each transition that now is accessible for the subject.

    The three events being dispatched are:

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

    Note

    The leaving and entering events are triggered even for transitions that stayin same place.

    Here is an example of how to enable logging for every time a "blog_publishing"workflow leaves a place:

    1. use Psr\Log\LoggerInterface;
    2. use Symfony\Component\EventDispatcher\EventSubscriberInterface;
    3. use Symfony\Component\Workflow\Event\Event;
    4.  
    5. class WorkflowLogger implements EventSubscriberInterface
    6. {
    7. private $logger;
    8.  
    9. public function __construct(LoggerInterface $logger)
    10. {
    11. $this->logger = $logger;
    12. }
    13.  
    14. public function onLeave(Event $event)
    15. {
    16. $this->logger->alert(sprintf(
    17. 'Blog post (id: "%s") performed transition "%s" from "%s" to "%s"',
    18. $event->getSubject()->getId(),
    19. $event->getTransition()->getName(),
    20. implode(', ', array_keys($event->getMarking()->getPlaces())),
    21. implode(', ', $event->getTransition()->getTos())
    22. ));
    23. }
    24.  
    25. public static function getSubscribedEvents()
    26. {
    27. return [
    28. 'workflow.blog_publishing.leave' => 'onLeave',
    29. ];
    30. }
    31. }

    There are a special kind of events called "Guard events". Their event listenersare invoked every time a call to Workflow::can, Workflow::apply orWorkflow::getEnabledTransitions is executed. With the guard events you mayadd custom logic to decide which transitions should be blocked or not. Here is alist of the guard event names.

    • workflow.guard
    • workflow.[workflow name].guard
    • workflow.[workflow name].guard.[transition name]This example stops any blog post being transitioned to "reviewed" if it ismissing a title:
    1. use Symfony\Component\EventDispatcher\EventSubscriberInterface;
    2. use Symfony\Component\Workflow\Event\GuardEvent;
    3.  
    4. class BlogPostReviewListener implements EventSubscriberInterface
    5. {
    6. public function guardReview(GuardEvent $event)
    7. {
    8. /** @var App\Entity\BlogPost $post */
    9. $post = $event->getSubject();
    10. $title = $post->title;
    11.  
    12. if (empty($title)) {
    13. // Block the transition "to_review" if the post has no title
    14. $event->setBlocked(true);
    15. }
    16. }
    17.  
    18. public static function getSubscribedEvents()
    19. {
    20. return [
    21. 'workflow.blog_publishing.guard.to_review' => ['guardReview'],
    22. ];
    23. }
    24. }

    Event Methods

    Each workflow event is an instance of Event.This means that each event has access to the following information:

    The execution of the workflow can be controlled by executing custom logic todecide if the current transition is blocked or allowed before applying it. Thisfeature is provided by "guards", which can be used in two ways.

    First, you can listen to the guard events.Alternatively, you can define a guard configuration option for thetransition. The value of this option is any valid expression created with the:

    1. # config/packages/workflow.yaml
    2. framework:
    3. workflows:
    4. blog_publishing:
    5. # previous configuration
    6. transitions:
    7. to_review:
    8. # the transition is allowed only if the current user has the ROLE_REVIEWER role.
    9. guard: "is_granted('ROLE_REVIEWER')"
    10. from: draft
    11. to: reviewed
    12. publish:
    13. # or "is_anonymous", "is_remember_me", "is_fully_authenticated", "is_granted", "is_valid"
    14. guard: "is_authenticated"
    15. from: reviewed
    16. to: published
    17. reject:
    18. # or any valid expression language with "subject" referring to the supported object
    19. guard: "has_role('ROLE_ADMIN') and subject.isRejectable()"
    20. from: reviewed
    21. to: rejected

    You can also use transition blockers to block and return a user-friendly errormessage when you stop a transition from happening.In the example we get this message from theEvent's metadata, giving you acentral place to manage the text.

    This example has been simplified; in production you may prefer to use the component to manage messages in oneplace:

    1. namespace App\Listener\Workflow\Task;
    2.  
    3. use Symfony\Component\EventDispatcher\EventSubscriberInterface;
    4. use Symfony\Component\Workflow\Event\GuardEvent;
    5. use Symfony\Component\Workflow\TransitionBlocker;
    6.  
    7. class BlogPostPublishListener implements EventSubscriberInterface
    8. {
    9. public function guardPublish(GuardEvent $event)
    10. {
    11. $eventTransition = $event->getTransition();
    12. $hourLimit = $event->getMetadata('hour_limit', $eventTransition);
    13.  
    14. if (date('H') <= $hourLimit) {
    15. return;
    16. }
    17.  
    18. // Block the transition "publish" if it is more than 8 PM
    19. // with the message for end user
    20. $explanation = $event->getMetadata('explanation', $eventTransition);
    21. $event->addTransitionBlocker(new TransitionBlocker($explanation , 0));
    22. }
    23.  
    24. public static function getSubscribedEvents()
    25. {
    26. return [
    27. 'workflow.blog_publishing.guard.publish' => ['guardPublish'],
    28. ];
    29. }
    30. }

    New in version 4.1: The transition blockers were introduced in Symfony 4.1.

    Usage in Twig

    Symfony defines several Twig functions to manage workflows and reduce the needof domain logic in your templates:

    • workflow_can()
    • Returns true if the given object can make the given transition.
    • workflow_transitions()
    • workflow_marked_places()
    • Returns an array with the place names of the given marking.
    • workflow_has_marked_place()
    • Returns true if the marking of the given object has the given state.The following example shows these functions in action:
    1. <h3>Actions on Blog Post</h3>
    2. {% if workflow_can(post, 'publish') %}
    3. <a href="...">Publish</a>
    4. {% endif %}
    5. {% if workflow_can(post, 'to_review') %}
    6. <a href="...">Submit to review</a>
    7. {% endif %}
    8. {% if workflow_can(post, 'reject') %}
    9. <a href="...">Reject</a>
    10. {% endif %}
    11.  
    12. {# Or loop through the enabled transitions #}
    13. {% for transition in workflow_transitions(post) %}
    14. <a href="...">{{ transition.name }}</a>
    15. {% else %}
    16. No actions available.
    17. {% endfor %}
    18.  
    19. {# Check if the object is in some specific place #}
    20. {% if workflow_has_marked_place(post, 'reviewed') %}
    21. <p>This post is ready for review.</p>
    22. {% endif %}
    23.  
    24. {# Check if some place has been marked on the object #}
    25. {% if 'reviewed' in workflow_marked_places(post) %}
    26. <span class="label">Reviewed</span>
    27. {% endif %}

    New in version 4.1: The feature to store metadata in workflows was introduced in Symfony 4.1.

    In case you need it, you can store arbitrary metadata in workflows, theirplaces, and their transitions using the metadata option. This metadata canbe as simple as the title of the workflow or as complex as your own applicationrequires:

    • YAML
    • XML
    1. <!-- config/packages/workflow.xml -->
    2. <?xml version="1.0" encoding="UTF-8" ?>
    3. <container xmlns="http://symfony.com/schema/dic/services"
    4. xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    5. xmlns:framework="http://symfony.com/schema/dic/symfony"
    6. xsi:schemaLocation="http://symfony.com/schema/dic/services https://symfony.com/schema/dic/services/services-1.0.xsd
    7. http://symfony.com/schema/dic/symfony https://symfony.com/schema/dic/symfony/symfony-1.0.xsd"
    8. >
    9. <framework:config>
    10. <framework:workflow name="blog_publishing">
    11. <framework:metadata>
    12. <framework:title>Blog Publishing Workflow</framework:title>
    13. </framework:metadata>
    14. <!-- ... -->
    15. <framework:place name="draft">
    16. <framework:metadata>
    17. <framework:max-num-of-words>500</framework:max-num-of-words>
    18. </framework:metadata>
    19. </framework:place>
    20. <!-- ... -->
    21. <framework:transition name="to_review">
    22. <framework:from>draft</framework:from>
    23. <framework:to>review</framework:to>
    24. <framework:metadata>
    25. <framework:priority>0.5</framework:priority>
    26. </framework:metadata>
    27. </framework:transition>
    28. <framework:transition name="publish">
    29. <framework:from>reviewed</framework:from>
    30. <framework:to>published</framework:to>
    31. <framework:metadata>
    32. <framework:hour_limit>20</framework:hour_limit>
    33. <framework:explanation>You can not publish after 8 PM.</framework:explanation>
    34. </framework:metadata>
    35. </framework:transition>
    36. </framework:workflow>
    37. </framework:config>
    38. </container>
    • PHP
    1. // config/packages/workflow.php
    2. $container->loadFromExtension('framework', [
    3. // ...
    4. 'workflows' => [
    5. 'blog_publishing' => [
    6. 'metadata' => [
    7. 'title' => 'Blog Publishing Workflow',
    8. ],
    9. // ...
    10. 'places' => [
    11. 'draft' => [
    12. 'metadata' => [
    13. 'max_num_of_words' => 500,
    14. ],
    15. ],
    16. // ...
    17. ],
    18. 'transitions' => [
    19. 'to_review' => [
    20. 'from' => 'draft',
    21. 'to' => 'review',
    22. 'metadata' => [
    23. 'priority' => 0.5,
    24. ],
    25. ],
    26. 'publish' => [
    27. 'from' => 'reviewed',
    28. 'to' => 'published',
    29. 'metadata' => [
    30. 'hour_limit' => 20,
    31. 'explanation' => 'You can not publish after 8 PM.',
    32. ],
    33. ],
    34. ],
    35. ],
    36. ],
    37. ]);

    Then you can access this metadata in your controller as follows:

    1. use App\Entity\BlogPost;
    2. use Symfony\Component\Workflow\Registry;
    3.  
    4. public function myController(Registry $registry, BlogPost $post)
    5. {
    6. $workflow = $registry->get($post);
    7.  
    8. $title = $workflow
    9. ->getMetadataStore()
    10. ->getWorkflowMetadata()['title'] ?? 'Default title'
    11. ;
    12.  
    13. $maxNumOfWords = $workflow
    14. ->getMetadataStore()
    15. ->getPlaceMetadata('draft')['max_num_of_words'] ?? 500
    16. ;
    17.  
    18. $aTransition = $workflow->getDefinition()->getTransitions()[0];
    19. $priority = $workflow
    20. ->getMetadataStore()
    21. ->getTransitionMetadata($aTransition)['priority'] ?? 0
    22. ;
    23. }

    There is a getMetadata() method that works with all kinds of metadata:

    1. // pass no arguments to getMetadata() to get "workflow metadata"
    2. $title = $workflow->getMetadataStore()->getMetadata()['title'];
    3.  
    4. // pass a string (the place name) to getMetadata() to get "place metadata"
    5. $maxNumOfWords = $workflow->getMetadataStore()->getMetadata('draft')['max_num_of_words'];
    6.  
    7. // pass a Transition object to getMetadata() to get "transition metadata"
    8. $priority = $workflow->getMetadataStore()->getMetadata($aTransition)['priority'];

    In a in your controller:

    1. // $transition = ...; (an instance of Transition)
    2.  
    3. // $workflow is a Workflow instance retrieved from the Registry (see above)
    4. $title = $workflow->getMetadataStore()->getMetadata('title', $transition);
    5. $this->addFlash('info', "You have successfully applied the transition with title: '$title'");

    Metadata can also be accessed in a Listener, from the Event object.

    Learn more