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:
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.