Skip to main content
A SHORT TALE ABOUT


STATE MACHINE
Once upon a time…
Kelly
final class Payment


{


private bool $pending = false;


}
Th
e end of chapter 1
A few days later
final class Payment


{


private bool $pending = false;


private bool $failed = false;


}
final class Payment


{


private bool $pending = false;


private bool $failed = false;


}
final class Payment


{


public const NEW = 1;


public const PENDING = 2;


public const FAILED = 3;


private int $state = self::NEW;


}
?
State Machine
(Q, Σ, δ, q0, F)


Q = a
fi
nite set of states


Σ = a
fi
nite, nonempty input alphabet


δ = a series of transition functions


q0 = the starting state


F = the set of accepting states
NEW
PENDING
FAILED PAID
Create
Pay
Fail
Let’s do the research!
De
fi
ne possible states and relation between them


Protect business rules


Integrate other services on transitions
What do we expect?
WinzouStateMachine
Callback’s execution in con
fi
g
fi
le


Used heavily in Sylius


Con
fi
gurable arguments of service


Services have to be public in order to integrate them with state machine


Without stable release, yet battle-tested and robust
$config =
[

'graph' => 'payment'
,

'property_path' => 'state'
,

'states' => ['new','pending','failed','paid']
,

'transitions' =>
[

'process' =>
[

'from' => ['new']
,

'to' => 'pending'
,

]
,

'fail' =>
[

'from' => ['pending']
,

'to' => 'failed'
,

]
,

'pay' =>
[

'from' => ['pending']
,

'to' => 'paid'
,

]
,

]
,

];
$config =
[

'graph' => 'payment'
,

'property_path' => 'state'
,

'states' => ['new','pending','failed','paid'],
'transitions' =>
[

'process' =>
[

'from' => ['new']
,

'to' => 'pending'
,

]
,

'fail' =>
[

'from' => ['pending']
,

'to' => 'failed'
,

]
,

'pay' =>
[

'from' => ['pending']
,

'to' => 'paid'
,

]
,

]
,

];
$config =
[

'graph' => 'payment'
,

'property_path' => 'state',
'states' => ['new','pending','failed','paid']
,

'transitions' => [
'process' =>
[

'from' => ['new']
,

'to' => 'pending'
,

]
,

'fail' =>
[

'from' => ['pending']
,

'to' => 'failed'
,

]
,

'pay' =>
[

'from' => ['pending']
,

'to' => 'paid'
,

]
,

]
,

];
$config =
[

'graph' => 'payment'
,

'property_path' => 'state'
,

'states' => ['new','pending','failed','paid'],
'transitions' =>
[

'process' =>
[

'from' => ['new']
,

'to' => 'pending'
,

]
,

'fail' =>
[

'from' => ['pending']
,

'to' => 'failed'
,

]
,

'pay' =>
[

'from' => ['pending']
,

'to' => 'paid'
,

]
,

]
,

];
Symfony Work
fl
ow
Maintained by Symfony


Flex support


Work
fl
ow & state machine support


Execution of external services with events


XML / YAML / PHP support out-of-the-box


Best documentation hands down
$definitionBuilder = new DefinitionBuilder()
;

$definition = $definitionBuilder->addPlaces(['new','pending','failed','paid']
)

->addTransition(new Transition('process', 'new', 'pending')
)

->addTransition(new Transition('fail', [‘pending'], 'failed')
)

->addTransition(new Transition('pay', 'pending', 'paid')
)

->build(
)

;

$singleState = true
;

$property = 'state'
;

$marking = new MethodMarkingStore($singleState, $property)
;

$workflow = new Workflow($definition, $marking, null, 'payment');
$definitionBuilder = new DefinitionBuilder()
;

$definition = $definitionBuilder->addPlaces(['new','pending','failed','paid']
)

->addTransition(new Transition('process', 'new', 'pending')
)

->addTransition(new Transition('fail', ['pending'], 'failed')
)

->addTransition(new Transition('pay', 'pending', 'paid')
)

->build(
)

;

$singleState = true
;

$property = 'state'
;

$marking = new MethodMarkingStore($singleState, $property)
;

$workflow = new Workflow($definition, $marking, null, 'payment');
$definitionBuilder = new DefinitionBuilder()
;

$definition = $definitionBuilder->addPlaces(['new','pending','failed','paid']
)

->addTransition(new Transition('process', 'new', 'pending')
)

->addTransition(new Transition('fail', [‘pending'], 'failed')
)

->addTransition(new Transition('pay', 'pending', 'paid')
)

->build(
)

;

$singleState = true
;

$property = 'state'
;

$marking = new MethodMarkingStore($singleState, $property)
;

$workflow = new Workflow($definition, $marking, null, 'payment');
Finite
Oldest and most popular implementation


(in terms of stars)


Least used implementation


(in terms of daily installs according to packagist)


Extendability with events & callbacks de
fi
nition


Perhaps reached its EOL


(https://github.com/yohang/Finite/issues/161)


It is said to be little bit heavier implementation compared to Winzou
Back to
the
st
ory…
final class Payment


{


public const NEW = 'new';


private string $state = self::NEW;


public function getState(): string


{


return $this->state;


}


public function setState(string $state): void


{


$this->state = $state


}


}
WinzouStateMachine
$payment = new Payment()
;

$stateMachine = new StateMachine($payment, $config)
;

$stateMachine->apply('process')
;

$stateMachine->apply('fail');
Symfony Work
fl
ow
$payment = new Payment()
;

$workflow->apply($payment, 'process')
;

$workflow->apply($payment, 'fail');
Th
e end of chapter 2
New adventur
e
WinzouStateMachine
final class InventoryOperato
r

{

public function __invoke(TransitionEvent $transitionEvent): voi
d

{

$stateMachine = $transitionEvent->getStateMachine()
;

/** @var Payment $payment *
/

$payment = $stateMachine->getObject()
;

// Reduce inventory of bought product
s

}

}
$config =
[

// ..
.

'callbacks' =>
[

'before' =>
[

'reduce_amount' =>
[

'on' => ['pay']
,

'do' => new InventoryOperator()
,

]
,

]
,

]
,

];
final class InventoryOperato
r

{

public function __invoke(TransitionEvent $transitionEvent): voi
d

{

$stateMachine = $transitionEvent->getStateMachine()
;

/** @var Payment $payment *
/

$payment = $stateMachine->getObject()
;

// Reduce inventory of bought product
s

}

}
$config =
[

// ..
.

'callbacks' =>
[

'before' =>
[

'reduce_amount' =>
[

'on' => ['pay']
,

'do' => new InventoryOperator()
,

]
,

]
,

]
,

];
final class InventoryOperato
r

{

public function __invoke(TransitionEvent $transitionEvent): voi
d

{

$stateMachine = $transitionEvent->getStateMachine()
;

/** @var Payment $payment *
/

$payment = $stateMachine->getObject()
;

// Reduce inventory of bought product
s

}

}
$config =
[

// ..
.

'callbacks' => [
'before' => [
'reduce_amount' =>
[

'on' => ['pay']
,

'do' => new InventoryOperator()
,

]
,

]
,

]
,

];
Guard


Before


After
Callbacks types
* Same can be achieved with dispatched events
class StateMachine implements StateMachineInterface


{


public function apply(/** */): void


{


// Guard callback


if (!$this->can($transition)) {


throw new SMException();


}


// Before callback


$this->setState('failed');


// After callback


}


}
final class InventoryOperato
r

{

public function __invoke(TransitionEvent $transitionEvent): voi
d

{

$stateMachine = $transitionEvent->getStateMachine()
;

/** @var Payment $payment *
/

$payment = $stateMachine->getObject()
;

// Reduce inventory of bought product
s

}

}
$config =
[

// ..
.

'callbacks' =>
[

'before' =>
[

'reduce_amount' => [
'on' => ['pay']
,

'do' => new InventoryOperator()
,

]
,

]
,

]
,

];
On -> transition


From -> state


To -> state
Callbacks types
Symfony Work
fl
ow
$dispatcher = new EventDispatcher()
;

$dispatcher->addListener
(

'workflow.payment.enter.paid',
 

new PaymentPaidListener(
)

)
;

$workflow = new Workflow($definition, $marking, $dispatcher, 'payment');
final class PaymentPaidListener


{


public function __invoke(EnterEvent $event): void


{


// Reduce inventory of bought products


}


}
$dispatcher = new EventDispatcher()
;

$dispatcher->addListener
(

'workflow.payment.enter.paid',
 

new PaymentPaidListener(
)

);
$workflow = new Workflow($definition, $marking, $dispatcher, 'payment');
final class PaymentPaidListener


{


public function __invoke(EnterEvent $event): void


{


// Reduce inventory of bought products


}


}
$dispatcher = new EventDispatcher()
;

$dispatcher->addListener(
'workflow.payment.enter.paid',
 

new PaymentPaidListener(
)

)
;

$workflow = new Workflow($definition, $marking, $dispatcher, 'payment');
final class PaymentPaidListener


{


public function __invoke(EnterEvent $event): void


{


// Reduce inventory of bought products


}


}
$dispatcher = new EventDispatcher()
;

$dispatcher->addListener(
'workflow.payment.enter.paid',
new PaymentPaidListener(
)

)
;

$workflow = new Workflow($definition, $marking, $dispatcher, 'payment');
final class PaymentPaidListener


{


public function __invoke(EnterEvent $event): void


{


// Reduce inventory of bought products


}


}
$dispatcher = new EventDispatcher()
;

$dispatcher->addListener(
'workflow.payment.enter.paid',
 

new PaymentPaidListener(
)

)
;

$workflow = new Workflow($definition, $marking, $dispatcher, 'payment');
final class PaymentPaidListener


{


public function __invoke(EnterEvent $event): void


{


// Reduce inventory of bought products


}


}
work
fl
ow.[place type]


work
fl
ow.[work
fl
ow name].[place type]


work
fl
ow.[work
fl
ow name].[place type].[place name]
Actions
leave -> current place


enter -> which place(places) will be visited


entered -> which place(places) were visited
Types for places
work
fl
ow.[transition type]


work
fl
ow.[work
fl
ow name].[transition type]


work
fl
ow.[work
fl
ow name].[transition type].[transition name]
Actions
guard -> possibility to validate transition


transition -> which transition will be executed


completed -> which transition was executed


announce -> which transition are available now
Types for transitions
class Workflow implements WorkflowInterface


{


public function apply(/** */
)

{

$this->leave(/** */)
;

$this->transition(/** */);
$this->enter(/** */)
;

/** Making transition */
$this->markingStore->setMarking($payment, 'failed', $context)
;

$this->entered(/** */)
;

$this->completed(/** */)
;

$this->announce(/** */)
;

}

}
WinzouStateMachine
final class BlockAuthorizer


{


public function __invoke(): bool


{


return false;


}


}
$config =
[

/** ... *
/

'callbacks' =>
[

/** ... *
/

'guard' =>
[

'guard-blocked' =>
[

'to' => ['blocked']
,

'do' => new BlockedAuthorizer()
,

]
,

]
,

]
,

];
Symfony Work
fl
ow
final class BlockedGuardListener


{


public function __invoke(GuardEvent $event): void


{


$event->setBlocked(true);


}


}
$dispatcher->addListener
(

'workflow.payment.guard.block'
,

new BlockedGuardListener(
)

)
;
Th
e end of chapter 3
Summary
De
fi
ne 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
WinzouStateMachine vs Symfony Work
fl
ow
Why should you care?
Common issues
Business logic wired up in
con
fi
guration
fi
les
Describe problem with state machine


but implement directly in code
Possibility to destroy state of entities


if state machine is omitted
😭
setState(…)
lchrusciel/RawStateMachine
@Sylius
Thank you!
@lukaszchrusciel