How to Use the Workflow
Edit this pageWarning: You are browsing the documentation for Symfony 3.3, which is no longer maintained.
Read the updated version of this page for Symfony 6.3 (the current stable version).
How to Use the 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. A post can have 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
# app/config/config.yml
framework:
workflows:
blog_publishing:
type: 'workflow' # or 'state_machine'
marking_store:
type: 'multiple_state' # or 'single_state'
arguments:
- 'currentPlace'
supports:
- AppBundle\Entity\BlogPost
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
<!-- app/config/config.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">
<framework:marking-store type="single_state">
<framework:argument>currentPlace</framework:argument>
</framework:marking-store>
<framework:support>AppBundle\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
// app/config/config.php
$container->loadFromExtension('framework', array(
// ...
'workflows' => array(
'blog_publishing' => array(
'type' => 'workflow', // or 'state_machine'
'marking_store' => array(
'type' => 'multiple_state', // or 'single_state'
'arguments' => array('currentPlace')
),
'supports' => array('AppBundle\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.
With this workflow named blog_publishing
, you can get help to decide
what actions are allowed on a blog post:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
$post = new \AppBundle\Entity\BlogPost();
$workflow = $this->container->get('workflow.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 $e) {
// ...
}
// 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 object 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 object 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 object entered a new place. This is the first event where the object is marked as being in the new place.
The three events being dispatched are:
workflow.enter
workflow.[workflow name].enter
workflow.[workflow name].enter.[place name]
workflow.entered
-
Similar to
workflow.enter
, except the marking store is updated before this event (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.announce
-
Triggered for each transition that now is accessible for the object.
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 a "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 \AppBundle\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.
3.3
The
getWorkflowName()
method was introduced in Symfony 3.3.
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. - .. versionadded:: 3.3
-
The
workflow_marked_places()
andworkflow_has_marked_place()
functions were introduced in Symfony 3.3.
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 %}