Using State Machines in Laravel

Jeff Madsen
ITNEXT
Published in
5 min readMay 20, 2019

--

“State Machine”

Does that send shivers of high-level, computer science theory dread through your body? Another one of those “I should dig into that someday” feelings?

Fear not! I’m going to walk you though a very simple but practical use case of these that will have you wondering how you ever got by without them.

Let’s start with a simpler description of what this is all about. Have you ever had a bug ticket to look into that was essentially, “This Order had a status of ‘cancelled’, but then it was somehow changed to ‘pending’. Please look into it.”?

This is what State Machines are designed to prevent. They are validation rules for your internal status changes. That’s pretty much all you need to understand at this point!

What we do is, with the use of a code library, define all the valid status changes in a process. Then any time you want to update the status of that entity, you first run it through your rule set to check if it is okay. This way as more and more business logic is added you always have an underlying safety check that your business workflow is being followed.

Here’s a very simple example that you can try out on a default Laravel installation without too much code. We have a case where our Users are military personal, each with a rank. Here are the business rules:

  1. The total list of ranks are: Private, Corporal A Class, Corporal B Class, Corporal C Class, Sergeant A Class, Sergeant B Class
  2. The rules are:

a) A Private can become a Corporal of any of the 3 classes.

b) A Corporal A Class or Corporal B Class can only become a Sergeant A Class; likewise, Corporal B Class or Corporal C Class can only become a Sergeant B Class (so a Corporal B Class can become either type of Sergeant).

c) There are no demotions.

To set this up, add a string field ‘rank ’ to your standard install Users table and then composer install https://github.com/sebdesign/laravel-state-machine

What we have just installed is actually just a service provider for Laravel over the real logic, found in the https://github.com/winzou/state-machine library, but we don’t need to worry about that right now. If you do ever want to use a state machine without Laravel, just go straight to that git repo instead.

The next step is to publish: `php artisan vendor:publish — provider=”Sebdesign\SM\ServiceProvider”`. This creates a config file at config/state-machine.php which is where we’ll set up out “validation rules”.

return [
'rank_graph' => [
'class' => App\User::class,

'property_path' => 'rank',

'states' => [
'Private',
'Corporal A Class',
'Corporal B Class',
'Corporal C Class',
'Sergeant A Class',
'Sergeant B Class',
],

'transitions' => [
'promote_private_to_corporal_a_class' => [
'from' => ['Private'],
'to' => 'Corporal A Class',
],
'promote_private_to_corporal_b_class' => [
'from' => ['Private'],
'to' => 'Corporal B Class',
],
'promote_private_to_corporal_c_class' => [
'from' => ['Private'],
'to' => 'Corporal C Class',
],
'promote_corporal_a_class_sergeant_a_class' => [
'from' => ['Corporal A Class'],
'to' => 'Sergeant A Class',
],
'promote_corporal_b_class_to_sergeant_a_class' => [
'from' => ['Corporal B Class'],
'to' => 'Sergeant A Class',
],
'promote_corporal_b_class_to_sergeant_b_class' => [
'from' => ['Corporal B Class'],
'to' => 'Sergeant B Class',
],
'promote_corporal_c_class_sergeant_b_class' => [
'from' => ['Corporal C Class'],
'to' => 'Sergeant B Class',
],
],
],
];

Let’s break that down. With State Machines, a single rule set is referred to as a “graph”. Our graph is called `rank_graph`; for us, it is the key value in this array and nothing more. We might have other sets describing other parts of the system that can be added on below in this same config array.

“Class” is the model we are checking against; “property_path” the field we hold the various ranks, statuses, whatever you want to call them. These do not need to be Eloquent models, by the way. “States” is a full list of all the possible ranks — it is just a enum, in a manner of speaking.

“Transitions” is where the magic happens. We set up all the allowed rank changes — “transition paths”. Each validation rule is a nested array that has 1) a label (typically a clear description of the change being checked) and a 2) from/to relationship that maps which “from” states are allowed to be changed to which “to” states. For example, the three “promote_private_to_corporal_*_class” transitions say that if the user is currently a ‘Private’ they can be changed to any of the three Corporal ranks. They cannot become Sergeants, however, and so those two ranks are not included in the “to” array.

Now that we have established the validation rules, we need to make sure our updates go through them. There are many different ways of setting this up (I’ll mention a couple of others at the end) but a very simple way is to add a function to your model. There are three different syntax you can use to do this:

// Style 1: Get the factory from the interfaceuse SM\Factory\FactoryInterface;
public function stateMachine()
{
$factory = app(CallbackFactoryInterface::class);
return $factory->get($this, 'rank_graph');
}
// Style 2: Or get the factory from the aliasuse SM\Factory\FactoryInterface;
public function stateMachine()
{
$factory = app('sm.factory');
return $factory->get($this, 'rank_graph');
}
// Style 3: Or get the state machine directly from the facadeuse Sebdesign\SM\Facade as StateMachine;
public function stateMachine()
{
return StateMachine::get($this, 'rank_graph');
}

This was the same array key we set as a name in our config above. This way we can call the state machine from our User instance and check and update the logic:

$user->update($input);
$user->stateMachine()->apply('promote_private_to_corporal_a_class');
$user->save();

The update() and save() are standard Laravel, for any other fields that might be updating. The middle line is where the state-machine.php config is checked to see if this particular change is allowed. Internally it will check the current rank, look at the rank we are trying to update to, and then either allow it to pass or throw an SMException error.

That’s it, really! There is much more you can do, but this will keep you safe. The next thing to do is set up a bunch of Unit Tests for your various states, such as:

/** @test */
public function it_can_promote_private_to_corporal_b_class()
{
$user = factory(User::class)->create(['rank' => 'Private']);

$user->update(['rank' => 'Corporal B Class']);
$user->stateMachine()->apply('promote_private_to_corporal_b_class');
$user->save();

$updated_user = User::find($user->id);
$this->assertEquals('Corporal B Class', $updated_user->rank);
}

Just like the transitions we made in the config file, these will be very similar to one another and not so much effort to set up. They’ll help us make sure if we do need to change our state machine logic, everything continues to work.

More Reading and Advanced Usages:

The above example was very simple and yet is in actual production on some of our sites (with actual project domain logic, of course). However, once you’ve set this up to experiment with you may find you want to make greater use of this library. If so, here are some more complex use cases you can continue your studies with:

https://github.com/sebdesign/laravel-state-machine The library we used for working with Laravel. You can see further examples of incorporating Laravel Gates and Policies, as well as adding callbacks.

https://gist.github.com/iben12/7e24b695421d92cbe1fec3eb5f32fc94 A full build example by Bence Ivady . He has also created a Trait that works in place of the stateMachine function I showed above.

https://github.com/winzou/state-machine The underlying State Machine library by https://twitter.com/a_bacco This is framework agnostic; go ahead and play with it in a simple php file!

https://en.wikipedia.org/wiki/Finite-state_machine For the more studious among you; a starting point for learning about State Machines in general.

Hope that helped!

--

--