Onlangs kwam ik voor de eerste keer in aanraking met finite state machines in Laravel terwijl ik onderzoek deed naar een implementatie voor één van onze klanten. In deze blog wil ik kort uitleggen wat zo’n state machine inhoudt en waarvoor we zijn functionaliteiten kunnen gebruiken.
Flow control met state machines in Laravel
Dus wat is het juist?
Om het in de woorden van wikipedia uit te leggen: een finite state machine is een abstract, wiskundig model voor het gedrag van een systeem waarbij het model bestaat uit een eindig aantal toestanden, overgangen tussen die toestanden en acties.
Simpel gezegd willen we dat onze applicatie een beperkt aantal uitkomsten aanbiedt aan de gebruiker. Tegelijkertijd willen we ook de mogelijkheden beperken om deze uitkomsten te bereiken. Een goed voorbeeld is de flow die je moet doorlopen wanneer je online een bestelling wil afronden. Een logische volgorde is bijvoorbeeld dat een gebruiker eerst zijn persoonsgegevens ingeeft, dan wordt doorgestuurd naar het betalingsplatform en daarna een bevestiging krijgt in zijn mailbox. Je wil uiteraard niet dat de gebruiker de betaalstap kan overslaan en zijn order onmiddellijk kan bevestigen.
Klinkt goed, ik wil een voorbeeld!
Laten we ons bovenstaande voorbeeld uitwerken. Om de state machine te implementeren in onze laravel applicatie gebruik ik de package laravel-state-machine van sebDesign. Het eerste wat we moeten opstellen is een state machine graph, dit laat onze applicatie weten welke flow hij moet volgen en dankzij de package kunnen we dit eveneens onmiddellijk koppelen aan ons laravel model.
<?php
return [
'order_graph' => [
'class' => Order::class,
'graph' => 'order_graph',
'property_path' => 'state',
'metadata' => [],
'states' => [
'checkout',
'pending-user-data',
'pending-payment',
'finished',
'cancelled'
],
'transitions' => [
'create' => [
'from' => ['checkout'],
'to' => 'input-user-data'
],
'input-user-data' => [
'from' => ['pending-user-data'],
'to' => 'pending-payment'
],
'payment-succeeded' => [
'from' => ['pending-payment'],
'to' => 'finished'
],
'payment-failed' => [
'from' => ['pending-payment'],
'to' => 'cancelled'
]
],
],
];
Voor deze simpele use-case kunnen we vijf states aanmaken en de bijhorende transities definiëren. Deze transities kunnen we nu gebruiken om onze state te updaten en onze logische flow aan te houden. Dit creëert leesbare en makkelijk onderhoudbare entrypoints voor onze functionaliteiten.
Extra functionaliteiten voor elke transitie
In veel gevallen zal je extra functionaliteiten willen toevoegen voor of na je transities. In de state graph is het mogelijk om callbacks in te stellen waarop je kan inhaken om zo extra code uit te voeren. Je wil de bestelling misschien nog valideren vooraleer je verder gaat naar een volgende stap of misschien wil je automatisch een bevestigingsmail versturen wanneer de betaling geslaagd is? Laten we dieper ingaan op deze twee voorbeelden.
'callbacks' => [
'guard' => [
'quard_on_creating' => [
'on' => 'create',
'do' => ,
'args' => ['object'],
],
],
'after' => [
[
'on' => 'payment-succeeded',
'do' => 'event',
'args' => ['"payment-succeeded"', 'event', 'object'],
],
]
]
In dit stukje code hebben we deze twee voorbeelden uitgewerkt. In het eerste deel willen we vooraleer we om de persoonsgegevens vragen, eerst de bestelling valideren. Hiervoor stellen we een guard in die in MyService de bestelling nogmaals zal nakijken op fouten. Als argument sturen we hier het Orderobject mee dat we wensen te valideren. Wanneer bepaalde producten niet meer beschikbaar zijn, kunnen we op deze manier mogelijks een bestelling stopzetten en de koper hiervan op de hoogte brengen..
In het tweede deel, sturen we een event uit wanneer onze betaling geslaagd is. We kunnen hierop makkelijk meerdere subscribers instellen die verschillende functionaliteiten gaan toepassen. Een van deze subscribers kan de bevestigingsmail afhandelen terwijl een andere een notificatie van de nieuwe bestelling geeft aan ons extern boekhoudprogramma.
Wanneer zou jij het gebruiken?
Dus, wanneer zou ik het gebruiken? Altijd wanneer een entiteit in je code meer dan twee states heeft!
Na mijn eerste contact met deze architectuur heb ik state machines in verschillende projecten geïmplementeerd. In mijn ervaring helpen ze een ingewikkelde flow te verduidelijken. De callbacks bieden mij een goede manier om complexe code op te splitsen en zorgen ervoor dat deze eveneens makkelijk uitbreidbaar is. Dit resulteert in meer onderhoudbare code en zorgt voor een duidelijke seperation of concerns.