Workflow Events - Part 1


In this video we are going to cover the numerous Events that are dispatched whenever we go through a transition. To quickly recap, a transition is the process of going from one place to another place / places.

There are a whole bunch of events dispatched whenever one of these transitions takes place. To illustrate all of this, let's start with a stripped down workflow definition:

# /app/config/workflows.yml

framework:
    workflows:
        customer_signup:
            supports: AppBundle\Entity\Customer
            places:
                - prospect
                - free_customer
                - awaiting_passport
                - awaiting_card_details
                - paying_customer
            transitions:
                sign_up:
                    from: prospect
                    to: free_customer
                request_account_upgrade:
                    from: free_customer
                    to:
                        - awaiting_passport
                        - awaiting_card_details
                vip_approval:
                    from: free_customer
                    to: paying_customer

And the associated diagram:

symfony workflow events tutorial diagram

We're going to cover the events dispatched during applying the transition of sign_up.

Leave Event

To begin with, the first set of events to be dispatched are those concerned with "Leaving" a place. In this instance, we would be Leaving the place of prospect.

Generically, at least three events would be dispatched:

  • workflow.leave
  • workflow.[workflow_name].leave
  • workflow.[workflow_name].leave.[place_name]

More specific to our particular workflow, these events would be:

  • workflow.leave
  • workflow.customer_signup.leave
  • workflow.customer_signup.leave.prospect

As covered in the guard videos, there are two of the more generic events, and then a more specific event.

The generic workflow.leave event applies to any workflow definition, any time we leave a place. Subscribing to this event would mean you would hear about any leave event that occurs across all configured workflow definitions in your application. In other words, if you had three workflow definitions defined, this would be dispatched for transitions inside all three workflows.

The slightly less generic, but still quite generic workflow.customer_signup.leave event is dispatched for every transition inside the customer_signup workflow definition. In other words, if you had three workflow definitions defined, this would only be dispatched for transitions inside the customer_signup workflow, not for the other two.

The more specific workflow.customer_signup.leave.prospect event would only be dispatched when leaving the place of prospect inside the customer_signup workflow. In my experience these more specific events have been the most frequently useful.

Note here that if we were currently in two or more places, a specific leave event for each place would be dispatched. If we were leaving two places - place_a, and place_b inside the customer_signup workflow, in total the events would be:

  • workflow.leave
  • workflow.customer_signup.leave
  • workflow.customer_signup.leave.place_a
  • workflow.customer_signup.leave.place_b

Interesting to note here is that as part of the leave method which handles dispatching these events, the subject (think: object going through the transition) would now be unmarked from its current place.

You can see the code for this here. Note this is for version 3.2.6.

Transition Event

The next set of events to be dispatched are those concerned with the specific Transition we are going through. In this instance, we would be Transition through sign_up.

Generically, three events would be dispatched:

  • workflow.transition
  • workflow.[workflow_name].transition
  • workflow.[workflow_name].transition.[transition_name]

More specific to our particular transition, these events would be:

  • workflow.transition
  • workflow.customer_signup.transition
  • workflow.customer_signup.transition.sign_up

Again, the workflow.transition event is dispatched for any transition, in any workflow in our application.

The workflow.customer_signup.transition event would be dispatched for any transition inside the customer_signup workflow.

And workflow.customer_signup.transition.sign_up would only be dispatched when transition through sign_up inside the customer_signup workflow.

You can see the code for this here. Note this is for version 3.2.6.

Enter Event

The next set of events to be dispatched are those concerned with the next Place (or Places) we are about to enter. In this instance, we would be Transition through sign_up.

Generically, at least three events would be dispatched:

  • workflow.enter
  • workflow.[workflow_name].enter
  • workflow.[workflow_name].enter.[place_name]

More specific to our particular transition, these events would be:

  • workflow.enter
  • workflow.customer_signup.enter
  • workflow.customer_signup.enter.free_customer

As above, the workflow.enter event is dispatched any time we enter a new place, in any workflow in our application.

The workflow.customer_signup.enter event would be dispatched any time we enter a new place inside the customer_signup workflow.

And workflow.customer_signup.enter.free_customer would only be dispatched when we enter the place of free_customer inside the customer_signup workflow.

As with the leave events, note here that if we were currently about to enter two or more places, a specific enter event for each place would be dispatched. If we were entering two places - place_x, and place_y inside the customer_signup workflow, in total the events would be:

  • workflow.enter
  • workflow.customer_signup.enter
  • workflow.customer_signup.enter.place_x
  • workflow.customer_signup.enter.place_y

You can see the code for this here. Note this is for version 3.2.6.

Interlude For Marking

At this point it's worth noting that our object would now be marked with the new place.

This step does not involve dispatching any events.

Entered Event - Symfony 3.3 Onwards

Newly added in Symfony 3.3 is the event of entered.

Again, this follows the same pattern we have already seen for enter, and leave.

A specific entered event is dispatched for each new place that our subject (think: object undergoing transition) has entered.

Note the past tense - entered vs enter. In entered our subject has now had its marking updated.

Generically, at least three events would be dispatched:

  • workflow.entered
  • workflow.[workflow_name].entered
  • workflow.[workflow_name].entered.[place_name]

More specific to our particular transition, these events would be:

  • workflow.entered
  • workflow.customer_signup.entered
  • workflow.customer_signup.entered.free_customer

As with leave and enter, the workflow.entered event is dispatched any time we have entered a new place, in any workflow in our application.

The workflow.customer_signup.entered event would be dispatched any time we have entered a new place inside the customer_signup workflow.

And workflow.customer_signup.entered.free_customer would only be dispatched when we have entered the place of free_customer inside the customer_signup workflow.

As with the enter events, note here that if we had currently entered two or more places, a specific entered event for each place would be dispatched. If we have entered two places - place_x, and place_y inside the customer_signup workflow, in total the events would be:

  • workflow.entered
  • workflow.customer_signup.entered
  • workflow.customer_signup.entered.place_x
  • workflow.customer_signup.entered.place_y

You can see the code for this here. Note this links to the specific commit hash which represented master at the time of writing. Unfortunately 3.3 has not yet been tagged.

Announce

The most peculiar of the events in my opinion is that of announce.

Unlike all the other steps, in this step only very specific events are dispatched. These are events that announce the next available transitions.

The events follow the format:

  • workflow.[workflow_name].announce.[available_transition]

An individual event is dispatched for each available transition.

Taking our example, the following two events would be dispatched:

  • workflow.customer_signup.announce.request_account_upgrade
  • workflow.customer_signup.announce.vip_approval

Again, these are the next available transitions (not places) after our subject has successfully entered the next place(s).

How This Works

Seeing how this fits together is easiest with some code. I would strongly suggest you do this in your own project, as at this stage there is still change underway with the Workflow Component, and your code may differ slightly.

The easiest way to do this, if in PHPStorm, is to hit ctrl+N, or cmd+N on a mac, and type in 'Workflow.php'. This will find the file the quickest.

// /vendor/symfony/symfony/src/Symfony/Component/Workflow/Workflow.php

/**
 * Fire a transition.
 *
 * @param object $subject        A subject
 * @param string $transitionName A transition
 *
 * @return Marking The new Marking
 *
 * @throws LogicException If the transition is not applicable
 * @throws LogicException If the transition does not exist
 */
public function apply($subject, $transitionName)
{
    $transitions = $this->getEnabledTransitions($subject);

    // We can shortcut the getMarking method in order to boost performance,
    // since the "getEnabledTransitions" method already checks the Marking
    // state
    $marking = $this->markingStore->getMarking($subject);

    $applied = false;

    foreach ($transitions as $transition) {
        if ($transitionName !== $transition->getName()) {
            continue;
        }

        $applied = true;

        $this->leave($subject, $transition, $marking);

        $this->transition($subject, $transition, $marking);

        $this->enter($subject, $transition, $marking);

        $this->markingStore->setMarking($subject, $marking);

        $this->entered($subject, $transition, $marking);

        $this->announce($subject, $transition, $marking);
    }

    if (!$applied) {
        throw new LogicException(sprintf('Unable to apply transition "%s" for workflow "%s".', $transitionName, $this->name));
    }

    return $marking;
}

It's also worth taking a look at one of the methods, in this case leave:

// /vendor/symfony/symfony/src/Symfony/Component/Workflow/Workflow.php

use Symfony\Component\Workflow\Event\Event;

// * snip *

private function leave($subject, Transition $transition, Marking $marking)
{
    $places = $transition->getFroms();

    if (null !== $this->dispatcher) {
        $event = new Event($subject, $marking, $transition, $this->name);

        $this->dispatcher->dispatch('workflow.leave', $event);
        $this->dispatcher->dispatch(sprintf('workflow.%s.leave', $this->name), $event);

        foreach ($places as $place) {
            $this->dispatcher->dispatch(sprintf('workflow.%s.leave.%s', $this->name, $place), $event);
        }
    }

    foreach ($places as $place) {
        $marking->unmark($place);
    }
}

The use of sprintf may be a touch confusing if you haven't seen that before. To clarify, the %s markers will be replaced with the given variables - in other words, the first %s will be replaced with the contents of $this->name, and the second %s with the contents of $place.

In this way the code is able generate our event names programatically.

Whatever the event name, the Event object itself is always the same - an instance of Symfony\Component\Workflow\Event\Event.

In the next video we will get on to how we can subscribe to some, or all of these events.

Code For This Course

Get the code for this course.

Episodes