Workflow Component Introduction & Demo


In this video we are going to take a look at the Workflow Component, an awesome new feature added into the Symfony framework from Symfony 3.2 onwards. Before we get started, I would like to say a huge thank you to Grégoire Pineau, and Fabien Potencier, along with all other contributors, reviewers, and testers, who played their parts in adding this wonderful component. Thank you.

Before we go any further, I imagine you might wish to know why you would want, or need to use the Workflow Component in your project. Good question.

A workflow allows you to explicitly define the process or lifecycle that your objects go through as they pass through your system.

By creating a workflow definition, you can define all the "legal moves" that your objects can start from, and go to, when they are in a given state. The proper term for these "legal moves" is Transitions.

Essentially the workflow component helps to standardise the next available step, or steps, that you objects can take.

As ever, this is much easier to see in action.

As this component is useful not just moving forwards, there has also been a bundle created to integrate the Workflow Component with Symfony 2.3+. I have no direct experience with this approach.

As part of the reveal of the Workflow Component at Symfony Live Paris 2016, Grégoire Pineau created a fantastic example application to demo the key features offered in this component.

I would strongly recommend you pull this code to your local machine, and review the various files as we go through this video. If you do pull the code, be aware that it comes expecting to connect to a Postgres DB. If you would prefer to use MySQL, that is also possible but you will need to make some changes to the config:

# /app/config/parameters.yml

# example settings, plug in your own DB server values

parameters:
    database_host: 192.168.0.21
    database_port: null
    database_name: sf_paris_workflow_demo
    database_user: dbuser
    database_password: dbpassword

And to config.yml:

# /app/config/config.yml

# Doctrine Configuration
doctrine:
    dbal:
        driver:   pdo_mysql
        host:     "%database_host%"
        port:     "%database_port%"
        dbname:   "%database_name%"
        user:     "%database_user%"
        password: "%database_password%"
        charset:  UTF8

Or, you could choose to browse the code via Github, and use the online demo application.

Rather than try to address all the terminology and configuration up front, we will cover each piece as it becomes directly applicable to what we are trying to do.

A Short Note on the Workflow Terminology

The Workflow Component is based on the 'Workflow net', which is a subclass of a Petri Net. The official Symfony documentation links to the Wikipedia entry for Petri nets, which has a section on the Formal Definitions, and Basic Terminology.

As I am not the most mathematically inclined, this page was both scary and confusing. I will therefore give my layman's interpretation of the terms as we come across them, but do feel free to shout up and correct me if I am wrong.

Starting With State Machines

There are two types of workflows available to us:

  • Workflows;
  • and State Machines

This is much easier to see visually by way of an example in Symfony configuration format:

framework:
    workflows:
        your_state_machine_name_here:
            type: state_machine

    # and / or

        your_workflow_name_here:
            type: workflow

By default, the type: workflow is assumed, so if you do not see an explicitly defined type, you can infer you are working with a type of workflow.

The key point to note is that a state_machine may only be in one place at once.

A workflow may be in multiple places concurrently.

The way I think about a place is as another name for state.

Again, better illustrated with a chunk of configuration:

framework:
    workflows:
        simple_state_machine_example:
            type: state_machine
            places:
                - a
                - b
                - c

If you have a workflow with the type of state_machine, you can only ever be in one of the three available places at any one time.

If you have a workflow with the type of workflow, you can be in more than one place at any given time.

This makes less sense in the abstract of a, b, and c, but when using a more 'real world' example, as covered in the video, of backlog, in_progress, and done, it does make more sense. You would never want to be both in_progress, and done. That would be terribly confusing, but is the sort of thing that seems to inevitably happen in home-rolled, custom solutions once real users get involved.

framework:
    workflows:
        task:
            type: state_machine
            places:
                - backlog
                - in_progress
                - done

Each of these individually named workflows - such as task above, and earlier simple_state_machine_example, and your_workflow_name_here, etc, are called "workflow definitions".

Transitioning Between Places

One of the nicest features of the Workflow Component is in its ability to output / dump individual workflow definitions to a standardised format, which can be converted into a pretty visual representation of this definition:

Symfony workflow component state machine example

At this point I want to point out that the image you see above is different to that used in the video. I will address this point at the end of this write-up. For now, just be aware that this is the same process.

What we can see here are our three places, along with the two transitions (the "legal moves") that define how an object can progress (transition) from one state (place) to the next.

Defined in configuration, this looks like:

framework:
    workflows:
        task:
            type: state_machine
            supports:
                - AppBundle\Entity\Task
            places:
                - backlog
                - in_progress
                - done
            transitions:
                processing:
                    from: backlog
                    to: in_progress
                done:
                    from: in_progress
                    to: done

Note the two new additions here.

Firstly we need to define which object / entity this workflow definition supports. Note that this is a list, and multiple objects / entities can be used in the same workflow definition.

Next, we have the transitions key. This is fairly self explanitory. These are the "legal moves" allowed, as defined in - to an extent - the sort of English that a less technically minded person might comprehend. We call these people Project Managers. Ha.

It's important to understand that there is no magic going on here. To transition between places, we must make a note of the current place that a given entity / object is at currently. We do this by marking the object / entity.

Hand Your Work In For Marking

The concept of Marking again comes from the Petri Net terminology.

Personally, this is my least favourite term in the Workflow Component. And thankfully we can change this with a little extra config, but for the moment, let's go with it, as it's official.

To keep note (or mark) where our object is in its journey through our workflow definition, the Workflow Component will need to mark (via the MarkingStore) the object with its current place.

By default the Workflow Component will expect a defined getter and setter of getMarking, and setMarking.

<?php

// /src/AppBundle/Entity/Task.php

namespace AppBundle\Entity;

use Doctrine\ORM\Mapping as ORM;
/**
 * @ORM\Table()
 * @ORM\Entity()
 */
class Task
{
    /**
     * @ORM\Column(type="integer")
     * @ORM\Id()
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    private $id;

    /** @ORM\Column(type="string", nullable=true) */
    private $marking;

    public function getMarking()
    {
        return $this->marking;
    }

    public function setMarking($marking)
    {
        $this->marking = $marking;
    }
}

Note that as our Task is part of a state_machine workflow definition, that the value of $marking is stored as a string. If we could be in multiple places at once, this would need to be an array of some kind (e.g. a JSON array).

The state_machine workflow type is in itself, incredibly useful.

However, as your business requirements inevitably grow, the extra flexibility provided by the type of workflow will become potentially the more frequently used of the two.

A Workflow Type of workflow

As mentioned earlier, workflow is considered the default workflow type.

And again, the main difference between workflow and state_machine is that a workflow may be in more than one place at any given time.

Symfony workflow component workflow example

Note here that after we make the transition from draft, we are in two places concurrently:

  • wait_for_journalist
  • wait_for_spellchecker

It was around this point that I started to get quite excited about the possibilities that the Workflow Component would open up for me in my own projects. I hope you feel the same way.

framework:
    workflows:
        article:
            type: workflow
            supports:
                - AppBundle\Entity\Article
            places:
                - draft
                - wait_for_journalist
                - wait_for_spellchecker
            transitions:
                request_review:
                    from: draft
                    to:
                        - wait_for_journalist
                        - wait_for_spellchecker

I have removed all the extra parts of this workflow definition in order to focus only on the parts relevant to our introduction.

We know of the type, and supports.

We know that we can define places.

We've also covered transitions, and how a transition has a from and a to.

The really cool part is that a from, and a to, can both take a list of places. In doing so, as soon as our entity / object transitions through request_review, it will be marked with both places.

However, in order to do so we would need an entity with a marking property capable of storing arrays:

<?php

namespace AppBundle\Entity;

use Doctrine\ORM\Mapping as ORM;

/**
 * @ORM\Table()
 * @ORM\Entity()
 */
class Article
{
    /**
     * @ORM\Column(type="integer")
     * @ORM\Id()
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    private $id;

    /** @ORM\Column(type="json_array", nullable=true) */
    private $marking;

    public function getId()
    {
        return $this->id;
    }

    public function getMarking()
    {
        return $this->marking;
    }

    public function setMarking($marking)
    {
        $this->marking = $marking;
    }
}

And with that we have covered a whole bunch of things from a very high level.

Whilst this demo code is fantastic for seeing how the Workflow Component can be used, personally I found it a little overwhelming to use a base for my learning. It does, however, make an absolutely fantastic reference guide.

With this in mind, in the very next video we will start by implementing our own state_machine workflow definition, diving a little deeper into where the options are coming from, what options are available, how to override the marking configuration, and so on.

Code For This Course

Get the code for this course.

Episodes