A SHORT TALE
ABOUT
STATE MACHINE
@lukaszchrusciel
Albert
@lukaszchrusciel
Once upon a time…
@lukaszchrusciel
final class Payment
{
/** @var bool */
private $pending = false;
}
@lukaszchrusciel
@lukaszchrusciel
A few days later
@lukaszchrusciel
final class Payment
{
/** @var bool */
private $pending = false;
/** @var bool */
private $failed = false;
}
@lukaszchrusciel
final class Payment
{
/** @var bool */
private $pending = false;
/** @var bool */
private $failed = false;
}
final class Payment
{
public const NEW = 1;
public const PENDING = 2;
public const FAILED = 3;
/** @var int */
private $state = self::NEW;
}
?
@lukaszchrusciel
State Machine
@lukaszchrusciel
(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
@lukaszchrusciel
NEW
PENDING
FAILED PAID
Create
PayFail
@lukaszchrusciel
Libraries
@lukaszchrusciel
What can we do with it?
Define possible states and relation between them
Protect business rules (with guards)
Trigger other services on transitions
@lukaszchrusciel
WinzouStateMachine
@lukaszchrusciel
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
@lukaszchrusciel
Symfony Workflow
@lukaszchrusciel
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
pay:
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
pay:
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
pay:
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
pay:
from: pending
to: paid
@lukaszchrusciel
Finite
@lukaszchrusciel
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
final class Payment
{
public const NEW = 'new';
/** @var string */
private $state = self::NEW;
public function getState(): string
{
return $this->state;
}
public function setState(string $state): void
{
$this->state = $state
}
}
@lukaszchrusciel
WinzouStateMachine
final class SomeService
{
public function __construct(FactoryInterface $factory)
{
$this->factory = $factory;
}
public function doStuff(): void
{
$stateMachine = $this->factory->get($payment, 'payment');
$stateMachine->apply('process');
$stateMachine->apply(‘fail');
}
}
@lukaszchrusciel
Symfony Workflow
final class SomeService
{
public function __construct(Registry $workflows)
{
$this->workflows = $workflows;
}
public function doStuff(): void
{
$stateMachine = $this->workflows->get($payment, 'payment');
$stateMachine->apply($payment, 'process');
$stateMachine->apply($payment, ‘fail');
}
}
@lukaszchrusciel
@lukaszchrusciel
@lukaszchrusciel
New adventures
@lukaszchrusciel
@lukaszchrusciel
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
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
@lukaszchrusciel
Callbacks types
Guard
Before
After
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
@lukaszchrusciel
Callbacks types
On -> transition
From -> state
To -> state
@lukaszchrusciel
Symfony Workflow
final class PaymentPaidListener
{
public function __invoke(EnterEvent $event): void
{
// Reduce inventory of bought products
}
}
AppEventListenerAfterPaymentPaidListener:
tags:
- name: kernel.event_listener
event: workflow.payment.entered.paid
method: __invoke
@lukaszchrusciel
Actions
workflow.[transition type]
workflow.[workflow name].[transition type]
workflow.[workflow name].[transition type].[transition name]
@lukaszchrusciel
Types for transitions
guard
transition
completed
announce
@lukaszchrusciel
Actions
workflow.[place type]
workflow.[workflow name].[place type]
workflow.[workflow name].[place type].[place name]
@lukaszchrusciel
Types for places
leave
enter
entered
@lukaszchrusciel
@lukaszchrusciel
@lukaszchrusciel
Symfony Workflow
@lukaszchrusciel
#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
@lukaszchrusciel
@lukaszchrusciel
#2
framework:
workflows:
payment:
...
transitions:
...
block:
guard: "is_granted('ROLE_ADMIN')"
from: pending
to: blocked
symfony/security-core
@lukaszchrusciel
@lukaszchrusciel
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"]
@lukaszchrusciel
@lukaszchrusciel
@lukaszchrusciel
Final boss
@lukaszchrusciel
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
@lukaszchrusciel
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
@lukaszchrusciel
@lukaszchrusciel
Summary
@lukaszchrusciel
Define possible states and relation between them
Protect business rules (with guards)
Trigger other services on transitions
Ease to add new possible transitions
Trigger other state changes as a reaction
@lukaszchrusciel
Common issues
@lukaszchrusciel
Business logic wired up in configuration files
@lukaszchrusciel
Describe problem with state machine
but implement directly in code
@lukaszchrusciel
Possibility to destroy state of entities
if state machine is omitted
@lukaszchrusciel
@lukaszchrusciel
😭
setState(…)
@lukaszchrusciel
Informing external system on transition callback
@lukaszchrusciel
Dispatching messages
on transition callbacks is not the best idea.
Defer external calls and ensure proper state.
@lukaszchrusciel
https://github.com/lchrusciel/StateMachine
Thank you!

A short tale about state machine