image

Reusable symfony/workflow configuration

This tutorial expects you to have a basic understanding of symfony and php in general!

If you use the symfony/workflow component you start by defining your workflow in the workflow.yaml config file. To use it, you need to remember your various states and transitions to use them.

I’m not a friend of having this string values scattered around in my code base (or in templates).

So I take a different approach to use the workflows.

PHP side of things

Let’s start with a simple state machine. I just reuse the example given in the symfony documentation.

 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
# config/packages/workflow.yaml
framework:
    workflows:
        pull_request:
            type: 'state_machine'
            marking_store:
                type: 'method'
                property: 'currentPlace'
            supports:
                - App\Entity\PullRequest
            initial_marking: start
            places:
                - start
                - coding
                - test
                - review
                - merged
                - closed
            transitions:
                submit:
                    from: start
                    to: test
                update:
                    from: [coding, test, review]
                    to: test
                wait_for_review:
                    from: test
                    to: review
                request_change:
                    from: review
                    to: coding
                accept:
                    from: review
                    to: merged
                reject:
                    from: review
                    to: closed
                reopen:
                    from: closed
                    to: review

That’s a lot of states and transitions to remember.

Let’s transfer this information to a reusable interface:

 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
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
namespace App\Entities;

interface PullRequestWorkflowInterface
{
    // Start with defining all states
    public const STATE_START = 'start';
    public const STATE_CODING = 'coding';
    public const STATE_TEST = 'test';
    public const STATE_REVIEW = 'review';
    public const STATE_MERGED = 'merged';
    public const STATE_CLOSED = 'closed';

    // Next we need all states as array
    public const STATES = [
        self::STATE_START,
        self::STATE_CODING,
        self::STATE_TEST,
        self::STATE_REVIEW,
        self::STATE_MERGED,
        self::STATE_CLOSED,
    ];

    // now lets define the transition names
    public const TRANSITION_SUBMIT = 'submit';
    public const TRANSITION_UPDATE = 'update';
    public const TRANSITION_WAIT_FOR_REVIEW = 'wait_for_review';
    public const TRANSITION_REQUEST_CHANGE = 'request_change';
    public const TRANSITION_ACCEPT = 'accept';
    public const TRANSITION_REJECT = 'reject';
    public const TRANSITION_REOPEN = 'reopen';

    // time for the transition rules
    public CONST TRANSITIONS = [
        self::TRANSITION_SUBMIT => [
            'from' => [self::STATE_START],
            'to' => self::STATE_TEST,
        ],
        self::TRANSITION_UPDATE => [
            'from' => [
                self::STATE_CODING,
                self::STATE_TEST,
                self::STATE_REVIEW,
            ],
            'to' => self::STATE_TEST,
        ],
        self::TRANSITION_WAIT_FOR_REVIEW => [
            'from' => [self::STATE_TEST],
            'to' => self::STATE_REVIEW,
        ],
        self::TRANSITION_REQUEST_CHANGE => [
            'from' => [self::STATE_REVIEW],
            'to' => self::STATE_CODING,
        ],
        self::TRANSITION_ACCEPT => [
            'from' => [self::STATE_REVIEW],
            'to' => self::STATE_MERGED,
        ],
        self::TRANSITION_REJECT => [
            'from' => [self::STATE_REVIEW],
            'to' => self::STATE_CLOSED,
        ],
        self::TRANSITION_REOPEN => [
            'from' => [self::STATE_CLOSED],
            'to' => self::STATE_REVIEW,
        ],
    ];

    // for completness - define a method for the marking_store
    public function getCurrentState(): string;

Much better. We now can use this interface even in multiple classes if needed.

All left todo is using the interface:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
namespace App\Entities;

class PullRequest implements PullRequestWorkflowInterface
{
    // [....]

    public function getCurrentState(): string
    {
        // or however you called your field
        return $this->currentState;
    }
}

This would already work - but we dont want to have two places to maintain our config. So let’s make the workflow.yaml use our constants too.

All we need in workflow.yaml is:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
framework:
    workflows:
        pull_request:
            type: 'state_machine'
            marking_store:
                type: 'method'
                property: 'currentState'
            supports:
                - App\Entities\PullRequest
            initial_marking: !php/const App\Entities\PullRequest::STATE_START
            places: !php/const App\Entities\PullRequest::STATES
            transitions: !php/const App\Entities\PullRequest::TRANSITIONS

That’s it. Tidy isn’t it?

But what now? Just use the public constants:

1
2
    if ($stateMachine->can($pullRequest, PullRequest::TRANSITION_REVIEW)) {
    }

even in your Templates

1
2
3
{% if workflow_transition(object, constant("App\\Entity\\PullRequest::TRANSITION_REJECT")) %}

{% endif %}

From now on, you can change any aspect of your state machine in one single place. Thanks to autocomplete you don’t have to remember each and every state or transition and you can’t make typing errors.

Dumping the workflow to a image still works of course. We just moved the definition from yaml to php.