A SHORT TALE
ABOUT
STATE MACHINE
Albert
Once upon a time…
final class Payment
{
/** @var bool */
private $pending = false;
}
A few days later
final class Payment
{
/** @var bool */
private $pending = false;
/** @var bool */
private $failed = false;
}
final class Payment
{
/** @var bool */
private $pending = false;
/** @var bool */
private $failed = false;
}
final class Payment
{
public const NEW = 1;
public const PENDING = 1;
public const FAILED = 2;
/** @var int */
private $state = self::NEW;
}
?
State Machine
(Q, Σ, δ, q0, F)
Q = a finite set of states
Σ = a finite, nonempty input alphabet
δ = a series of transition functions
q0 = the starting state
F = the set of accepting states
NEW
PENDING
FAILED PAID
Create
PayFail
Libraries
What can we do with it?
Define possible states and relation between them
Protect business rules (with guards)
Trigger other services on transitions
WinzouStateMachine
Callback’s execution in config file
Used heavily in Sylius
Configurable arguments of service
Services have to be public in order to make it work
Lack of documentation
Not stable
winzou_state_machine:
payment:
class: AppModelPayment
property_path: state
graph: payment
states:
new: ~
pending: ~
failed: ~
paid: ~
transitions:
process:
from: [new]
to: pending
fail:
from: [pending]
to: failed
pay:
from: [pending]
to: paid
winzou_state_machine:
payment:
class: AppModelPayment
property_path: state
graph: payment
states:
new: ~
pending: ~
failed: ~
paid: ~
transitions:
process:
from: [new]
to: pending
fail:
from: [pending]
to: failed
pay:
from: [pending]
to: paid
winzou_state_machine:
payment:
class: AppModelPayment
property_path: state
graph: payment
states:
new: ~
pending: ~
failed: ~
paid: ~
transitions:
process:
from: [new]
to: pending
fail:
from: [pending]
to: failed
pay:
from: [pending]
to: paid
winzou_state_machine:
payment:
class: AppModelPayment
property_path: state
graph: payment
states:
new: ~
pending: ~
failed: ~
paid: ~
transitions:
process:
from: [new]
to: pending
fail:
from: [pending]
to: failed
pay:
from: [pending]
to: paid
Symfony Workflow
Maintained by Symfony
Flex support
Workflow & state machine support
Execution of external services with events
XML / YAML / PHP support out-of-the-box
framework:
workflows:
payment:
type: 'state_machine'
supports:
- AppModelPayment
initial_marking: new
marking_store:
type: method
property: state
places:
- new
- pending
- failed
- paid
transitions:
process:
from: new
to: pending
fail:
from: pending
to: failed
paying:
from: pending
to: paid
framework:
workflows:
payment:
type: 'state_machine'
supports:
- AppModelPayment
initial_marking: new
marking_store:
type: method
property: state
places:
- new
- pending
- failed
- paid
transitions:
process:
from: new
to: pending
fail:
from: pending
to: failed
paying:
from: pending
to: paid
framework:
workflows:
payment:
type: 'state_machine'
supports:
- AppModelPayment
initial_marking: new
marking_store:
type: method
property: state
places:
- new
- pending
- failed
- paid
transitions:
process:
from: new
to: pending
fail:
from: pending
to: failed
paying:
from: pending
to: paid
framework:
workflows:
payment:
type: 'state_machine'
supports:
- AppModelPayment
initial_marking: new
marking_store:
type: method
property: state
places:
- new
- pending
- failed
- paid
transitions:
process:
from: new
to: pending
fail:
from: pending
to: failed
paying:
from: pending
to: paid
Finite
Oldest and most popular implementation (in terms of stars)
Extendability with events & callbacks definition
It is said to be little bit heavier implementation compared to
Winzou
New adventures
Symfony Workflow
final class PaymentPaidListener
{
public function __invoke(EnterEvent $event): void
{
$payment = $event->getSubject();
// Reduce inventory of bought products
}
}
AppEventListenerPaymentPaidListener:
tags:
- name: kernel.event_listener
event: workflow.payment.enter.paid
method: __invoke
Actions
workflow.[type]
workflow.[workflow name].[type]
workflow.[workflow name].[type].[transition name]
Types for transitions
guard
transition
completed
announce
Actions
workflow.[type]
workflow.[workflow name].[type]
workflow.[workflow name].[type].[place name]
Types for places
leave
enter
entered
WinzouStateMachine
AppOperatorInventoryOperator:
public: true
final class InventoryOperator
{
public function __invoke(Payment $payment): void
{
// Reduce inventory of bought products
}
}
winzou_state_machine:
payment:
…
callbacks:
before:
reduce_amount:
on: ["pay"]
do: [“@AppOperatorInventoryOperator”, "__invoke"]
args: [“object"] # <- Expression language can be used
Callbacks types
Before
After
Guard
Callbacks types
On -> transition
From -> state
To -> state
Symfony Workflow
#1
final class BlockedGuardListener
{
public function __invoke(GuardEvent $event): void
{
$event->setBlocked(true);
}
}
AppEventListenerBlockedGuardListener:
tags:
- name: kernel.event_listener
event: workflow.payment.guard.block
method: __invoke
#2
framework:
workflows:
payment:
...
transitions:
...
block:
guard: "is_granted('ROLE_ADMIN')"
from: pending
to: blocked
symfony/security-core
WinzouStateMachine
AppAuthorizerBlockedAuthorizer:
public: true
final class BlockAuthorizer
{
public function __invoke(): bool
{
return false;
}
}
winzou_state_machine:
payment:
...
callbacks:
guard:
guard-blocked:
to: ["blocked"]
do: ["@AppAuthorizerBlockedAuthorizer", "__invoke"]
😭
Final boss
final class AfterPaymentPaidListener
{
public function __invoke(EnteredEvent $event): void
{
/** @var Payment $payment */
$payment = $event->getSubject();
file_get_contents(
'https://workflow.free.beeceptor.com?state=' . $payment->getState()
);
}
}
AppEventListenerAfterPaymentPaidListener:
tags:
- name: kernel.event_listener
event: workflow.payment.entered.paid
method: __invoke
But when do you flush?
final class InformAboutPaidPayment
{...}
final class InformAboutPaidPaymentHandler implements MessageHandlerInterface
{
public function __invoke(InformAboutPaidPayment $payment)
{
// Check if it should be dispatched
file_get_contents(
‘https://workflow.free.beeceptor.com?state='. $payment->getSubject()
);
}
}
MESSENGER_TRANSPORT_DSN=doctrine://default
Summary
Define possible states and relation between them
Protect business rules (with guards)
Trigger other services on transitions
Trigger other state changes as a reaction
Common issues
Business logic wired up in configuration files
Describe problem with state machine
but implement directly in code
Possibility to destroy state of entities
if state machine is omitted
setState(…)
Informing external system on transition callback
Sending emails or dispatching messages
on transition callbacks is not the best idea.
Defer external calls and ensure proper state.
https://github.com/lchrusciel/StateMachine
@lukaszchrusciel
Thank you!
https://github.com/lchrusciel/StateMachine
@lukaszchrusciel
Q & A
https://github.com/lchrusciel/StateMachine
@lukaszchrusciel
Thank you!

A short tale about state machine