Skip to content
  • About
    • What is Symfony?
    • Community
    • News
    • Contributing
    • Support
  • Documentation
    • Symfony Docs
    • Symfony Book
    • Screencasts
    • Symfony Bundles
    • Symfony Cloud
    • Training
  • Services
    • Platform.sh for Symfony Best platform to deploy Symfony apps
    • SymfonyInsight Automatic quality checks for your apps
    • Symfony Certification Prove your knowledge and boost your career
    • SensioLabs Professional services to help you with Symfony
    • Blackfire Profile and monitor performance of your apps
  • Other
  • Blog
  • Download
sponsored by SensioLabs
  1. Home
  2. Documentation
  3. Workflow
  4. How to Create and Use Workflows
  • Documentation
  • Book
  • Reference
  • Bundles
  • Cloud

Table of Contents

  • Installation
  • Creating a Workflow
  • Using a Workflow
  • Using Events
    • Guard Events
    • Event Methods
  • Usage in Twig

How to Create and Use Workflows

Edit this page

Warning: You are browsing the documentation for Symfony 4.0, which is no longer maintained.

Read the updated version of this page for Symfony 6.3 (the current stable version).

How to Create and Use Workflows

Installation

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

1
$ composer require symfony/workflow

Creating a Workflow

A workflow is a process or a lifecycle that your objects go through. Each step 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 needs a Definition and a way to write the states to the objects (i.e. an instance of a MarkingStoreInterface.)

Consider the following example for a blog post that can have these places: draft, review, rejected, published. You can define the workflow like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# config/packages/workflow.yaml
framework:
    workflows:
        blog_publishing:
            type: 'workflow' # or 'state_machine'
            audit_trail: 'enabled'
            marking_store:
                type: 'multiple_state' # or 'single_state'
                arguments:
                    - 'currentPlace'
            supports:
                - App\Entity\BlogPost
            initial_place: draft
            places:
                - draft
                - review
                - rejected
                - published
            transitions:
                to_review:
                    from: draft
                    to:   review
                publish:
                    from: review
                    to:   published
                reject:
                    from: review
                    to:   rejected
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
<!-- config/packages/workflow.xml -->
<?xml version="1.0" encoding="utf-8" ?>
<container xmlns="http://symfony.com/schema/dic/services"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:framework="http://symfony.com/schema/dic/symfony"
    xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd
        http://symfony.com/schema/dic/symfony http://symfony.com/schema/dic/symfony/symfony-1.0.xsd"
>

    <framework:config>
        <framework:workflow name="blog_publishing" type="workflow" audit_trail="enabled">
            <framework:marking-store type="single_state">
              <framework:argument>currentPlace</framework:argument>
            </framework:marking-store>

            <framework:support>App\Entity\BlogPost</framework:support>

            <framework:place>draft</framework:place>
            <framework:place>review</framework:place>
            <framework:place>rejected</framework:place>
            <framework:place>published</framework:place>

            <framework:transition name="to_review">
                <framework:from>draft</framework:from>

                <framework:to>review</framework:to>
            </framework:transition>

            <framework:transition name="publish">
                <framework:from>review</framework:from>

                <framework:to>published</framework:to>
            </framework:transition>

            <framework:transition name="reject">
                <framework:from>review</framework:from>

                <framework:to>rejected</framework:to>
            </framework:transition>

        </framework:workflow>

    </framework:config>
</container>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// config/packages/workflow.php

$container->loadFromExtension('framework', array(
    // ...
    'workflows' => array(
        'blog_publishing' => array(
            'type' => 'workflow', // or 'state_machine'
            'audit_trail' => 'enabled',
            'marking_store' => array(
                'type' => 'multiple_state', // or 'single_state'
                'arguments' => array('currentPlace')
            ),
            'supports' => array('App\Entity\BlogPost'),
            'places' => array(
                'draft',
                'review',
                'rejected',
                'published',
            ),
            'transitions' => array(
                'to_review' => array(
                    'from' => 'draft',
                    'to' => 'review',
                 ),
                 'publish' => array(
                     'from' => 'review',
                     'to' => 'published',
                 ),
                 'reject' => array(
                     'from' => 'review',
                     'to' => 'rejected',
                 ),
             ),
         ),
     ),
 ));
1
2
3
4
5
6
7
class BlogPost
{
    // This property is used by the marking store
    public $currentPlace;
    public $title;
    public $content;
}

Note

The marking store type could be "multiple_state" or "single_state". A single state marking store does not support a model being on multiple places at the same time.

Tip

The type (default value single_state) and arguments (default value marking) attributes of the marking_store option are optional. If omitted, their default values will be used.

Tip

Setting the audit_trail option to enabled makes the application generate detailed log messages for the workflow activity.

Using a Workflow

Once the blog_publishing workflow has been created, you can now use it to decide what actions are allowed on a blog post. For example, inside a controller of an application using the default services.yaml configuration, you can get the workflow by injecting the Workflow registry service:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// ...
use Symfony\Component\Workflow\Registry;
use App\Entity\BlogPost;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\Workflow\Exception\LogicException;

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 (LogicException $exception) {
            // ... if the transition is not allowed
        }

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

Using Events

To make your workflows more flexible, you can construct the Workflow object with an EventDispatcher. You can now create event listeners to block transitions (i.e. depending on the data in the blog post) and do additional actions when a workflow operation happened (e.g. sending announcements).

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 following order:

workflow.guard

Validate whether the transition is allowed at all (see below).

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.transition
  • 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 just before the subject places are updated, which means that the marking of the subject is not yet updated with the new places.

The three events being dispatched are:

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

The subject has entered in the places and the marking is updated (making it a good place to flush data in Doctrine).

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 stay in same place.

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
use Psr\Log\LoggerInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Workflow\Event\Event;

class WorkflowLogger implements EventSubscriberInterface
{
    public function __construct(LoggerInterface $logger)
    {
        $this->logger = $logger;
    }

    public function onLeave(Event $event)
    {
        $this->logger->alert(sprintf(
            'Blog post (id: "%s") performed transaction "%s" from "%s" to "%s"',
            $event->getSubject()->getId(),
            $event->getTransition()->getName(),
            implode(', ', array_keys($event->getMarking()->getPlaces())),
            implode(', ', $event->getTransition()->getTos())
        ));
    }

    public static function getSubscribedEvents()
    {
        return array(
            'workflow.blog_publishing.leave' => 'onLeave',
        );
    }
}

Guard Events

There are a special kind of events called "Guard events". Their event listeners are invoked every time a call to Workflow::can, Workflow::apply or Workflow::getEnabledTransitions is executed. With the guard events you may add custom logic to decide what transitions that are valid or not. Here is a list of the guard event names.

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

See example to make sure no blog post without title is moved to "review":

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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)) {
            // Posts with no title should not be allowed
            $event->setBlocked(true);
        }
    }

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

Event Methods

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

getMarking()
Returns the Marking of the workflow.
getSubject()
Returns the object that dispatches the event.
getTransition()
Returns the Transition that dispatches the event.
getWorkflowName()
Returns a string with the name of the workflow that triggered the event.

For Guard Events, there is an extended class GuardEvent. This class has two more methods:

isBlocked()
Returns if transition is blocked.
setBlocked()
Sets the blocked value.

Usage in Twig

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

workflow_can()
Returns true if the given object can make the given transition.
workflow_transitions()
Returns an array with all the transitions enabled for the given object.
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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<h3>Actions</h3>
{% if workflow_can(post, 'publish') %}
    <a href="...">Publish article</a>
{% endif %}
{% if workflow_can(post, 'to_review') %}
    <a href="...">Submit to review</a>
{% endif %}
{% if workflow_can(post, 'reject') %}
    <a href="...">Reject article</a>
{% endif %}

{# Or loop through the enabled transitions #}
{% for transition in workflow_transitions(post) %}
    <a href="...">{{ transition.name }}</a>
{% else %}
    No actions available.
{% endfor %}

{# Check if the object is in some specific place #}
{% if workflow_has_marked_place(post, 'to_review') %}
    <p>This post is ready for review.</p>
{% endif %}

{# Check if some place has been marked on the object #}
{% if 'waiting_some_approval' in workflow_marked_places(post) %}
    <span class="label">PENDING</span>
{% endif %}
This work, including the code samples, is licensed under a Creative Commons BY-SA 3.0 license.
TOC
    Version
    We stand with Ukraine.
    Version:
    Symfony Code Performance Profiling

    Symfony Code Performance Profiling

    Check Code Performance in Dev, Test, Staging & Production

    Check Code Performance in Dev, Test, Staging & Production

    Symfony footer

    ↓ Our footer now uses the colors of the Ukrainian flag because Symfony stands with the people of Ukraine.

    Avatar of juliendidier, a Symfony contributor

    Thanks juliendidier for being a Symfony contributor

    1 commit • 2 lines changed

    View all contributors that help us make Symfony

    Become a Symfony contributor

    Be an active part of the community and contribute ideas, code and bug fixes. Both experts and newcomers are welcome.

    Learn how to contribute

    Symfony™ is a trademark of Symfony SAS. All rights reserved.

    • What is Symfony?

      • Symfony at a Glance
      • Symfony Components
      • Case Studies
      • Symfony Releases
      • Security Policy
      • Logo & Screenshots
      • Trademark & Licenses
      • symfony1 Legacy
    • Learn Symfony

      • Symfony Docs
      • Symfony Book
      • Reference
      • Bundles
      • Best Practices
      • Training
      • eLearning Platform
      • Certification
    • Screencasts

      • Learn Symfony
      • Learn PHP
      • Learn JavaScript
      • Learn Drupal
      • Learn RESTful APIs
    • Community

      • SymfonyConnect
      • Support
      • How to be Involved
      • Code of Conduct
      • Events & Meetups
      • Projects using Symfony
      • Downloads Stats
      • Contributors
      • Backers
    • Blog

      • Events & Meetups
      • A week of symfony
      • Case studies
      • Cloud
      • Community
      • Conferences
      • Diversity
      • Documentation
      • Living on the edge
      • Releases
      • Security Advisories
      • SymfonyInsight
      • Twig
      • SensioLabs
    • Services

      • SensioLabs services
      • Train developers
      • Manage your project quality
      • Improve your project performance
      • Host Symfony projects

      Deployed on

    Follow Symfony

    Search by Meilisearch