A Simple State Machine Example

This video is available to view for members only.

Click here to Join!

Already a member?

Login


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">&times;</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:

chess knight legal moves

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:

  1. Use a Symfony command to dump out the Workflow definition into a .dot file
  2. 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:

simple state machine workflow dump example

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:

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.


Code For This Course

Get the code for this course.

Share This Episode

If you have found this video helpful, please consider sharing. I really appreciate it.


Episodes in this series

# Title Duration
1 Workflow Component Introduction & Demo 07:11
2 A Simple State Machine Example 09:38
3 Creating More Complex Workflows 07:58
4 Two Ways To Make Life Easier For You, The Developer 04:13
5 How To: Transitions That Split Into Two Or More Places 07:31
6 All I Need Are Your Bank Account Details and Sort Code Number, Madam 07:32
7 Your Passport To Freedom 13:37
8 Workflow Guards - Part 1 05:52
9 Workflow Guards - Part 2 07:12
10 Workflow Events - Part 1 02:34
11 Workflow Events - Part 2 - Custom Audit Trail Listener 05:13
12 New In Symfony 3.3 - Workflow Guard Expressions 03:11