A Simple State Machine Example
In this video we are going to create a simple example of the state_machine
workflow type, covering each of the important pieces of the workflow definition along the way.
In my opinion it is much easier to begin learning about the Workflow Component in a brand new project rather than trying to add the component into an existing project.
With that in mind, to follow along with this video, all you need is to run symfony new simple-state-machine-example
, let the installer do its thing, and then make a few small changes:
<!-- /app/Resources/views/base.html.twig -->
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>{% block title %}Welcome!{% endblock %}</title>
{% block stylesheets %}{% endblock %}
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.css">
<link rel="icon" type="image/x-icon" href="{{ asset('favicon.ico') }}" />
</head>
<body>
<div class="container">
<div class="row">
<div class="col-sm-12">
{% include ('::flash-messages.html.twig') %}
</div>
</div>
<div class="row">
<div class="col-sm-12">
{% block body %}
{% endblock %}
</div>
</div>
</div><!-- /.container -->
{% block javascripts %}{% endblock %}
</body>
</html>
And also add in the flash-messages.html.twig
template, as we will want some nicely styled output a little later.
<!-- /app/Resources/views/flash-messages.html.twig -->
{% block flash_messages %}
{% for type, messages in app.session.flashbag.all() %}
{% for message in messages %}
<div class="alert alert-{{ type }} alert-dismissible" role="alert">
<button type="button" class="close" data-dismiss="alert"><span aria-hidden="true">×</span><span class="sr-only">Close</span></button>
{{ message | raw }}
</div>
{% endfor %}
{% endfor %}
{% endblock %}
We will shortly need an entity creating, so be sure to setup your parameters.yml
file accordingly:
# /app/config/parameters.yml
parameters:
database_host: your-db-host-ip-here
database_port: ~
database_name: your_db_name
database_user: your_db_username
database_password: your_db_password
You can either generate an entity using the php bin/console doctrine:generate:entity
command, or manually define an entity file. For reference, all we need is an entity with an $id
property at this time:
<?php
// /src/AppBundle/Entity/SimpleStateMachineExample.php
namespace AppBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* SimpleStateMachineExample
*
* @ORM\Table(name="simple_state_machine_example")
* @ORM\Entity()
*/
class SimpleStateMachineExample
{
/**
* @var int
*
* @ORM\Column(name="id", type="integer")
* @ORM\Id
* @ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* Get id
*
* @return int
*/
public function getId()
{
return $this->id;
}
}
This entity name is poorly named. Feel free to use a better one.
With this most basic of setups in place, we can go ahead and create our first workflow definition.
A Simple State Machine Workflow Definition
To begin with, I'm going to add my workflow configuration directly inside config.yml
:
# /app/config/config.yml
framework:
# lots of
# other symfony framework
# config here
workflows:
simple_state_machine_example:
type: 'state_machine'
However, almost immediately this feels nasty. A better convention is to extract the workflows
section out into its own file, and import that as a separate resource instead. So let's do that:
# /app/config/config.yml
imports:
- { resource: parameters.yml }
- { resource: security.yml }
- { resource: services.yml }
- { resource: workflow.yml } # our new line
And with that in place, we can create a new file - workflow.yml
- inside the /app/config
directory, and move our workflow definition(s) into that instead:
# /app/config/workflow.yml
framework:
workflows:
simple_state_machine_example:
type: 'state_machine'
One key point - remember to include the top level framework
key, or this won't work!
Before we move on, let's quickly examine this workflow.yml
configuration file as it stands currently.
We have defined one workflow - simple_state_machine_example
. This is the name of our workflow definition, and will be used throughout our project whenever we need to access this specific workflow. Therefore, a descriptive name is important.
An example of getting
this service might be in our controller action:
$stateMachine = $this->get('state_machine.simple_state_machine_example');
Note there the prefix of state_machine
. We will come back to this shortly.
Anyway, next up we have to explicitly define that this workflow definition is for a workflow type
of state_machine
.
The default option - and the implicit workflow type
used if we are not explicit about this being of type: state_machine
is type: workflow
.
As you can see from the controller action example above, the resulting service name for this workflow definition will be a concatenation of type.definition_name
, as in our case, state_machine.simple_state_machine_example
.
Our simple_state_machine_example
workflow definition is not yet complete. Let's continue adding config:
# /app/config/workflow.yml
framework:
workflows:
simple_state_machine_example:
type: 'state_machine'
places:
- a
- b
- c
transitions:
start:
from: a
to: b
end:
from: b
to: c
We've added in three places
:
a
b
- and
c
The terminology for places
, and transitions
comes from the formal definitions defined by the Petri Net, on which the Workflow Component is based.
Essentially we have transitions
, which allow our object to move from
and to
a set of defined places
. Your object may never be in the place
of start
, nor end
.
Taken in a different context, we might write a workflow definition for the Knight chess piece given the following image:

framework:
workflows:
black_knight_chess_movement_example:
type: 'state_machine'
places:
- d4 # starting point
- c6
- e6
- b5
- f5
- b3
- f3
- c2
- e2
transitions:
d4_c6:
from: d4
to: c6
d4_e6:
from: d4
to: e6
d4_b5:
from: d4
to: b5
# ... and so on
Of course, this would be crazy given that as soon as the piece is moved, all these definitions become largely useless. But it's just an example... maybe a bad one :)
It does cover the use case where we wouldn't ever want our Knight to be in two places at once though.
Also it covers the rules of the Knight. A knight can only move in an 'L' shape. Not every square is a legal move. There are rules, and we must follow them!
Can I Transition?
As we saw a little earlier, once we have a valid workflow definition, we are able to ask Symfony for a service that represents this workflow in code.
Jumping into a controller now, let's continue our example:
<?php
// /src/AppBundle/Controller/DefaultController.php
namespace AppBundle\Controller;
use AppBundle\Entity\SimpleStateMachineExample;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Workflow\Exception\LogicException;
class DefaultController extends Controller
{
/**
* @Route("/", name="homepage")
*/
public function indexAction(Request $request)
{
$entity = new SimpleStateMachineExample();
$stateMachine = $this->get('state_machine.simple_state_machine_example');
// interesting things here
return $this->render('default/index.html.twig');
}
}
To strip away any potential confusion here, before going further let's peek behind the scenes, directly into the container definition for this new service:
➜ state-machine-example git:(master) php bin/console debug:container state
Select one of the following services to display its information [state_machine.simple_state_machine_example]:
[0] state_machine.simple_state_machine_example
> 0
Information for Service "state_machine.simple_state_machine_example"
====================================================================
------------------ --------------------------------------------
Option Value
------------------ --------------------------------------------
Service ID state_machine.simple_state_machine_example
Class Symfony\Component\Workflow\StateMachine
Tags -
Public yes
Synthetic no
Lazy no
Shared yes
Abstract no
Autowired no
Autowiring Types -
------------------ --------------------------------------------
Immediately then we can see this $stateMachine
variable is going to contain an instance of a StateMachine
.
Knowing this, we can look at the StateMachine
class to determine all the available methods:
getMarking
can
apply
getEnabledTransitions
getName
getDefinition
Sure, we might not know what each method does at this point, but there's no mystery as to where things are happening.
To begin with, let's use the can
method to determine what our $entity
can, and cannot do:
<?php
// /src/AppBundle/Controller/DefaultController.php
public function indexAction(Request $request)
{
$entity = new SimpleStateMachineExample();
$stateMachine = $this->get('state_machine.simple_state_machine_example');
$transitions['can_start'] = $stateMachine->can($entity, 'start');
$transitions['can_end'] = $stateMachine->can($entity, 'end');
return $this->render('default/index.html.twig', [
'transitions' => $transitions,
]);
}
Note here that the $transitions
variable is nothing to do with the state machine implementation. It's just a name I have invented to contain data related to the outcome of checking whether we can, or cannot make the requested transitions.
We're then passing this variable into our Twig template under the key of transitions
.
Interestingly, at this point this will blow up.
We have not yet added the required getMarking
, and setMarking
methods to our $entity
:
<?php
// /src/AppBundle/Entity/SimpleStateMachineExample.php
namespace AppBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* SimpleStateMachineExample
*
* @ORM\Table(name="simple_state_machine_example")
* @ORM\Entity()
*/
class SimpleStateMachineExample
{
/**
* @var int
*
* @ORM\Column(name="id", type="integer")
* @ORM\Id
* @ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* @var string
*
* @ORM\Column(name="marking", type="string", length=255)
*/
private $marking;
/**
* Get id
*
* @return int
*/
public function getId()
{
return $this->id;
}
/**
* @return string
*/
public function getMarking()
{
return $this->marking;
}
/**
* @param string $marking
*
* @return SimpleStateMachineExample
*/
public function setMarking($marking)
{
$this->marking = $marking;
return $this;
}
}
Remembering to inform Doctrine of our changes:
php bin/console doctrine:schema:update --force
In the real world, please use migrations.
Now, marking
is really not my favourite name for this at all, but it's the official terminology, so for the moment, that's what we are going to use.
This should now work, but with very little to show for it in the browser.
What we now need is some visual representation of our transitional capabilities. Of course, Twig has functions available to help us with this, but for the moment let's pretend we aren't aware of them, and instead, create our own:
<!-- /app/Resources/views/default/index.html.twig -->
{% extends 'base.html.twig' %}
{% block body %}
<div id="wrapper">
<div id="container">
<ul>
{% for transition, isAvailable in transitions %}
<li>{{ transition | replace({'_': ' '}) | title }}? <i class="fa fa-{{ isAvailable ? 'check' : 'times' }}" aria-hidden="true"></i></li>
{% endfor %}
</ul>
</div>
</div>
{% endblock %}
As a quick overview as to what's happening here, we are passing through an array of our transitions data as an array.
This array contains two keys - can_start
, and can_end
, and the associated bool
values returned by the outcomes of the two can
method calls.
We are using the Twig for
method with their key, value
representation to help us loop through these transitions.
In the first loop through, transition
will contain can_start
.
This looks ugly, so we pass this key through Twig's replace
filter to convert this value to Can start
. I want both words to be title cased, and so I then pass this output through Twig's title filter to achieve just that.
Now we have the key of Can Start
.
isAvailable
will contain the boolean output of our can
method call from the controller action.
Using the ternary operator, we can switch between check
and times
to display a font awesome tick, or cross (times
) icon.
Not the most elegant of code, sir, but it checks out.
Then, because this is a for
loop, we repeat this exact same process for can_end
.
You should now be able to see a tick icon next to Can Start?
, and a cross icon next to Can End?
.
Changing Our Starting place
Let's say we have defined our workflow definition as above, but the business rules change and we now need to start from the place
of b
:
# /app/config/workflow.yml
framework:
workflows:
simple_state_machine_example:
type: 'state_machine'
places:
- a
- b
- c
transitions:
start:
from: a
to: b
end:
from: b
to: c
Well, we could explicitly call the setMarking
method on our $entity
inside our controller:
<?php
// /src/AppBundle/Controller/DefaultController.php
public function indexAction(Request $request)
{
$entity = new SimpleStateMachineExample();
$entity->setMarking('b');
$stateMachine = $this->get('state_machine.simple_state_machine_example');
$transitions['can_start'] = $stateMachine->can($entity, 'start');
$transitions['can_end'] = $stateMachine->can($entity, 'end');
return $this->render('default/index.html.twig', [
'transitions' => $transitions,
]);
}
Of course, this isn't great. We don't want to have to remember to do this everywhere we new
up a SimpleStateMachineExample
.
Thankfully, a piece of configuration exists that we can add to our workflow definition to address this very problem:
# /app/config/workflow.yml
framework:
workflows:
simple_state_machine_example:
type: 'state_machine'
places:
- a
- b
- c
initial_place: b
transitions:
start:
from: a
to: b
end:
from: b
to: c
Simple enough.
Now, refreshing the page should show a cross icon next to Can Start?
, and a tick icon next to Can End?
.
Likewise, if you set the initial_place: c
, then both Can Start?
and Can End?
should have a cross icon.
Changing From marking
One of my personal bug bears is in the use of the term marking
, and having to have a getMarking
, and setMarking
method on my entities. It's not language that I would use, and it feels weird to me.
Thankfully, again, changing this is very simple:
# /app/config/workflow.yml
framework:
workflows:
simple_state_machine_example:
type: 'state_machine'
places:
- a
- b
- c
initial_place: b
marking_store:
type: "single_state"
arguments: ['status']
transitions:
start:
from: a
to: b
end:
from: b
to: c
And we also need to update our entity accordingly:
<?php
// /src/AppBundle/Entity/SimpleStateMachineExample.php
namespace AppBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* SimpleStateMachineExample
*
* @ORM\Table(name="simple_state_machine_example")
* @ORM\Entity()
*/
class SimpleStateMachineExample
{
/**
* @var int
*
* @ORM\Column(name="id", type="integer")
* @ORM\Id
* @ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* @var string
*
* @ORM\Column(name="status", type="string", length=255)
*/
private $status;
/**
* Get id
*
* @return int
*/
public function getId()
{
return $this->id;
}
/**
* @return string
*/
public function getStatus()
{
return $this->status;
}
/**
* @param string $status
*
* @return SimpleStateMachineExample
*/
public function setStatus($status)
{
$this->status = $status;
return $this;
}
}
Again, don't forget to inform Doctrine of your changes:
php bin/console doctrine:schema:update --force
There's no magic or hidden knowledge involved in knowing about this. You can dump the container configuration reference and see this for yourself:
➜ state-machine-example git:(master) ✗ php bin/console config:dump-reference framework
# Default configuration for extension with alias: "framework"
framework:
# snip
workflows:
# Prototype
name:
type: workflow # One of "workflow"; "state_machine"
marking_store:
type: ~ # One of "multiple_state"; "single_state"
arguments: []
service: ~
supports: [] # Required
initial_place: null
places: [] # Required
transitions: # Required
# Prototype
-
name: ~ # Required
from: []
to: []
Now, this dumps out an absolute ton of stuff, so you will need to scroll up about half way back to find this. But it's all there for you to see.
To use the property of status
, rather than marking
, we need to simply pass in status
as the first argument into the arguments
of marking_store
.
We might also wish to explicitly define our marking_store
as single_state
.
Both of these points become clearer if we look at the SingleStateMarkingStore
class.
Firstly, the SingleStateMarkingStore
is only concerned with subjects that can be in one, and only one state (place
) at any given time. This sounds like the state_machine
!
Next, if we look at the __construct
method, we can see that by default, the $property
(first argument) is set to marking
. By passing in status
to the marking_store.arguments
key, we can override this default value.
Creating Workflow Diagrams
To finish up in this video we cover how to create diagrams from our workflow definitions.
As part of this process you will need to have a third party piece of software installed - Graphviz.
Installing Graphviz on Ubuntu is as easy as:
sudo apt-get install graphviz
And assuming you have Homebrew installed on your Mac, the process is similarly easy:
brew install graphviz
Further installation instructions are available covering Windows, Centos, and Solaris.
Creating a workflow diagrams at this point is a two step process:
- Use a Symfony command to dump out the Workflow definition into a
.dot
file - Use
dot
command line application (graphviz) to convert the.dot
file into a graphic
To complete step 1, we will need the name of the workflow we wish to dump. In our case, this will be simple_state_machine_example
. The command therefore will be:
php bin/console workflow:dump simple_state_machine_example > out.dot
You can - of course - use any name for the output file that you wish.
Again, we can remove any mystery as to the process here by running the command without sending the output into out.dot
:
➜ state-machine-example git:(master) php bin/console workflow:dump simple_state_machine_example
digraph workflow {
ratio="compress" rankdir="LR"
node [fontsize="9" fontname="Arial" color="#333333" fillcolor="lightblue" fixedsize="1" width="1"];
edge [fontsize="9" fontname="Arial" color="#333333" arrowhead="normal" arrowsize="0.5"];
place_a [label="a", shape=circle, style="filled"];
place_b [label="b", shape=circle];
place_c [label="c", shape=circle];
place_a -> place_b [label="start" style="solid"];
place_b -> place_c [label="end" style="solid"];
}
This would be the contents written to the out.dot
file. This is config that graphviz can interpret and translate into a graphic.
Assuming you have used the first command to create the out.dot
file, converting this file is a little nerdy, but thankfully, largely copy / paste:
dot -Tpng out.dot -o simple_state_machine_example.png
Again, of course, feel free to change the output (-o
) filename to something that makes sense for you. You don't need to stick to .png
files, you can use any of the supported output formats.
And with that you should have a lovely visual representation of your workflow dumped into your working directory. Here's mine:

Now one thing to note here:
If you are wondering why this diagram doesn't have squares representing the transition points, I - unfortunately - cannot give you a definitive answer. It's extra confusing as the previous video does demonstrate the use of a State Machine workflow dump which does have squares to represent the transitions.
That said, after doing quite a bit of digging I can see that there are two different implementations of the DumperInterface
:
- https://github.com/symfony/workflow/blob/master/Dumper/GraphvizDumper.php
- https://github.com/symfony/workflow/blob/master/Dumper/StateMachineGraphvizDumper.php
In every attempt I have made, the State Machine workflow type does use the StateMachineGraphvizDumper
, as you might expect. In the Symfony Live Paris 2016 demo application (as covered in the previous video) however, this uses a custom output method which appears to use the GraphvizDumper
for both workflow types.
This is extra confusing to me currently as the official documentation appears to also use the GraphvizDumper
in the diagram in their documentation the State Machine workflow. I need to do further digging into why this is, but for now, be aware that this issue exists.