# Implementing State Machine On Eloquent Model Some days ago I came across a task where I needed to implement managable state for an Eloquent model. This is a common task, actually there is a mathematical model called "[Finite-state Machine](https://en.wikipedia.org/wiki/Finite-state_machine)". The concept is that the state machine (SM) _"can be in exactly one of the finite number of states at any given time"_. Also changing from one state to another (called _transition_) depends on fulfilling the conditions defined by its configuration. Practically this means you define each state that the SM can be in and the possible transitions. To define a transition you set the states on which the transition can be applied (initial conditions) and the *only* state in which the SM should be after the transition. That's the theory, let's get to the work. ## Dependency setup Since SM is a common task, we can choose existing implementations using `composer` packages. We are on Laravel, so I searched for Laravel SM packages and found the [sebdesign/laravel-state-machine](https://github.com/sebdesign/laravel-state-machine) package, that is a Laravel service provider for the [winzou/state-machine](https://github.com/winzou/state-machine) package. So let's `require` that: ``` $ composer require sebdesign/laravel-state-machine ``` Then we have to register the provider and facade in the configuration: ```php // config/app.php 'providers' => [ Sebdesign\SM\ServiceProvider::class, ], 'aliases' => [ 'StateMachine' => Sebdesign\SM\Facade::class, ], ``` and publish the confiuration file to `config/state-machine.php`: ``` $ php artisan vendor:publish --provider="Sebdesign\SM\ServiceProvider" ``` Now we can instantiate SMs by calling the `get` method on `SM\FactoryInterface` like this: ```php $sm = app(FactoryInterface::class)->get($object,$graph); ``` where `$object` is the entity whose state we want to manage and `$graph` is the configuration of the possible states and transitions to be used. ## Configuration and migrations Let's assume we have a model in our app that needs managed state. A typical case for this is an Order. It should be in exactly one state at any time and there are strict rules from which state to what state it can get (ex. a _shipped_ order cannot be _cancelled_, but it can get _delivered_ and from there it can be _returned_). We will define the configuration first. Opening `config/state-machine.php` you will see an example configuration provided by the package, named `graphA`. The config returns an array of _graphs_. You can get rid of the predefined _graph_ or leave it as is for future reference. Let's create a new _graph_ for our `Order` model: ```php // config/state_machine.php return [ 'order' => [ 'class' => App\Order::class, 'property_path' => 'last_state', 'states' => [ 'new', 'processed', 'cancelled', 'shipped', 'delivered', 'returned' ], 'transitions' => [ 'process' => [ 'from' => ['new'], 'to' => 'processed' ], 'cancel' => [ 'from' => ['new','processed'], 'to' => 'cancelled' ], 'ship' => [ 'from' => ['processed'], 'to' => 'shipped' ], 'deliver' => [ 'from' => ['shipped'], 'to' => 'delivered' ], 'return' => [ 'from' => ['delivered'], 'to' => 'returned' ] ] ], //... ] ``` As I assumed, we have this `App\Order::class` and its migration already, but we defined the `property_path` to be `last_state` in the above config. This means SM will look for a `last_state` property on the object and use that for storing the state. We need to add that. Create the migration first: ``` $ php artisan make:migration add_last_state_to_orders ``` then edit it: ```php // database/migrations/yyyy_mm_dd_hhmmss_add_last_state_to_orders_table.php use Illuminate\Support\Facades\Schema; use Illuminate\Database\Schema\Blueprint; use Illuminate\Database\Migrations\Migration; class AddLastStateToOrdersTable extends Migration { public function up() { Schema::table('orders', function($table) { $table->string('last_state')->default('new'); }); } public function down() { Schema::table('oders', function($table) { $table->dropColumn('last_state'); }); } } ``` We also want to store the _state history_ for our orders, so that we can see who and when initiated transitions on a given entity. As we are at it, let's make a model and migration for that as well: ``` $ php artisan make:model OrderState -m ``` This will create a model class in `app/OrderState.php` and a migration. Edit the migration first: ```php // database/migrations/yyyy_mm_dd_hhmmss_create_order_states_table.php use Illuminate\Support\Facades\Schema; use Illuminate\Database\Schema\Blueprint; use Illuminate\Database\Migrations\Migration; class CreateOrderStatesTable extends Migration { public function up() { Schema::create('order_states', function (Blueprint $table) { $table->increments('id'); $table->integer('order_id'); $table->string('transition'); $table->string('to'); $table->integer('user_id'); $table->timestamps(); }); } public function down() { Schema::dropIfExists('order_states'); } } ``` then run ``` $ php artisan migrate ``` Now let's see the `OrderState` model! We need to set up relations to the order and the user: ```php // app/OrderState.php namespace App; use Illuminate\Database\Eloquent\Model; class OrderState extends Model { protected $fillable = ['transition','from','user_id','order_id','to']; public function order() { return $this->belongsTo('App\Order'); } public function user() { return $this->belongsTo('App\User'); } } ``` That's all, now we can start implementing the SM. ## The _Statable_ Trait First I just wrote some methods in the `Order` model to manage the SM, but then I thought: this feels like a drop-in feature, that can be added to any model. And this calls for a `Trait`. So let' create the `Statable` trait: ```php // app/Traits/Statable.php namespace App\Traits trait Statable { } ``` When I first added the SM directly in the model I used its `__constructor()` method to instantiate the SM and store it in a property. However calling the `__constructor()` in a trait doesn't seem to be a good idea, so we need another approach to make sure the SM gets set up and stored, but instantiated only once. Let's create a `stateMachine` method for this purpose: ```php // app/Traits/Statable.php namespace App\Traits use SM\Factory\FactoryInterface; trait Statable { /** * @var StateMachine $stateMachine */ protected $stateMachine; public function stateMachine() { if (!$this->stateMachine) { $this->stateMachine = app(FactoryInterface::class)->get($this, self::SM_CONFIG); } return $this->stateMachine; } } ``` Don't forget to import the SM's `FactoryInterface`! Here we check if the model has a SM already and if not we get one from the factory using the model object and the _graph_ as parameters. The _graph_ should be specified in the model class as the `SM_CONFIG` constant. I made the method public so that we can interact with the SM from the model in a fluent way (like `$order->getStateMachine()->getState()`), however we will implement convenient methods to access some interactions with short syntax. First will be the `stateIs()` method. ```php // app/Traits/Statable.php public function stateIs() { return $this->getStateMachine()->getState(); } ``` Now we can call this method on the model to get the current state: ```php $currentState = $order->stateIs(); ``` Next we need a method for applying a transition, let it be `transition()`: ```php // app/Traits/Statable.php public function transition($transition) { return $this->getStateMachine()->apply($transition); } ``` so we can use it like: ``` $order->transition('process'); ``` Also we have a method on the SM that helps determine if a transition can be applied on the current state. We will also expose this directly on the model with the `transitionAllowed()` method that wraps the SM's `can()` method: ```php // app/Traits/Statable.php public function transitionAllowed($transition) { return $this->getStateMachine()->can($transition); } ``` One last thing we need is to set up is the state history relation. Since this is tightly coupled with the SM, we can create its method in this trait: ```php // app/Traits/Statable.php public function history() { return $this->hasMany(self::HISTORY_MODEL['name']); } ``` As you can see the related model class will be configured on the model itself by the `HISTORY_MODEL` constant along with `SM_CONFIG`. We are ready with this trait, now we can use it in the model. ## The Order model Basically now we just let our model use the `Statable` trait and define the config needed by the trait. ```php // app/Order.php namespace App; use Traits\Statable; use Illuminate\Database\Eloquent\Model; class Order extends Model { use Statable; const HISTORY_MODEL = [ 'name' => 'App\OrderState' // the related model to store the history ]; const SM_CONFIG = 'order'; // the SM graph to use // other relations and methods of the model } ``` Now we can manage the state of the model like this: ```php $order = App\Order::first(); try { $order->transition('process'); } catch (Exception $e) { // if the transition cannot be applied on the current state or it does not exist // SM will throw an SMException instance // we can handle it here } $order->save(); ``` Or alternatively we can check if the transition can be applied first: ```php $order = App\Order::first(); if ($order->transitionAllowed('process') { $order->transition('process'); $order->save(); } else { // hanle rejection } ``` This will now update the `last_state` property of the model and by calling the `save` method it also persists it in the DB. However we do not store the history yet. ## Storing State History Every time our model changes state through a transition of the SM we need to add a row to the `order_states` table saving the transition, the state we got in, the user who initated it and when this happened. Writing this manually every time you apply a transition can become tedious, we need something better. Fortunately our SM fires `Events` if you interact with it and we can use them to call the tasks we have to do every time. SM has the following events: * `TEST_TRANSITION` fired when we call the `can()` method, or our wrapper for that: `transitionAllowed()` * `PRE_TRANSITION` fired before any transition * `POST_TRANSITION` fired after any transition We will use the `POST_TRANSITION` event and set up a listener for that. First we register our listener: ```php // app/Providers/EventServiceProvider.php protected $listen = [ SMEvents::POST_TRANSITION => [ 'App\Listeners\StateHistoryManager@postTransition', ], ]; ``` Let's create that class: ```php // app/Listeners/StateHistoryManager.php namespace App\Listeners; use SM\Event\TransitionEvent; class StateHistroyManager { public function postTransition(TransitionEvent $event) { $sm = $event->getStateMachine(); $model = $sm->getObject(); $model->addHistoryLine([ "transition" => $event->getTransition(), "to" => $sm->getState() ]); } } ``` Since the `Event` contains the SM instance and that contains the model instance our job is easy. We get the model, we save it and create a history relation on it that is filled up with the data. So, from now on we don't even have to bother with saving the model after a state change. Let's make a method for this: ```php // app/Traits/Statable.php public function addHistoryLine(array $transitionData) { $transitionData['user_id'] = auth()->id(); $this->save(); return $this->history()->create($transitionData); } ``` NOTE: here we assumed that we have a logged in user. It makes sense to assume a state change can be triggered by an autheticated user only (maybe with proper role), so it is on you to ensure this. You may also wonder why don't we just make this in the listener, but you will find out later. ## Let's use it! Now that everything is ready we can use our SM. Here is an example with a route closure: ```php // routes/web.php // the URL would be like: /order/8/process Route::get('/order/{order}/{transition}', function (App\Order $order, $transition) { // make sure you have autheticated user by route middleware or Auth check try { $order->transition($transition); } catch(Exception $e) { return abort(500, $e->getMessage()); } return $order->history()->get(); }); ``` The response will be something like the following `json`: ```json [ { "id": 1, "order_id": "8", "transition": "process", "to": "processed", "user_id": 1, "created_at": "2017-02-02 15:55:01", "updated_at": "2017-02-02 15:55:01" } ] ``` ## Taking it further You may have noticed that at some points (configuring `HISTORY_MODEL` and with the `addHistoryLine()` method) we could be more simple or specific if we're using just `Eloquent\Model` anyway. However with a little addition to our trait we would be able to use it on *any* type of object, rather than just `Models`. Fisrt we need a method to determine whether we are working with an `Eloquent\Model`. ```php // app/Traits/Statable.php protected function isEloquent() { return $this instanceof \Illuminate\Database\Eloquent\Model; } ``` Now we can improve the `history` and `addHistoryLine` methods to have *non-eloquent-compatible* implementations as well. First `history()` will return either a relation or the `HISTORY_MODEL` filtered for the actual object. ```php // app/Traits/Statable.php public function history() { if ($this->isEloquent()) { return $this->hasMany(self::HISTORY_MODEL['name']); } /** @var \Eloquent $model */ $model = app(self::HISTORY_MODEL['name']); return $model->where(self::HISTORY_MODEL['foreign_key'], $this->{self::PRIMARY_KEY}); } ``` For this we have to configure the `HISTORY_MODEL['foreign_key']` and `PRIMARY_KEY` on non-eloquent objects which will be used to save the history. The `addHistoryLine` method would look like this: ```php // app/Traits/Statable.php public function addHistoryLine(array $transitionData) { $transitionData['user_id'] = auth()->id(); if ($this->isEloquent()) { $this->save(); return $this->history()->create($transitionData); } $transitionData[self::HISTORY_MODEL['foreign_key']] = $this->{self::PRIMARY_KEY}; /** @var \Eloquent $model */ $model = app(self::HISTORY_MODEL['name']); return $model->create($transitionData); } ``` IMPORTANT: in case of non-eloquent objects you have to handle the persisting of the object itself with its changed `last_state` property. ## Testing We can test the model's _statable_ behaviour with PHPUnit. Let's create the test: ``` $ php artisan make:test StatableOrderTest --unit ``` This will create our test class in the file `tests/Unit/StatableOrderTest.php`. Besides the default imports we will need the following (if you use an IDE you can import these as you use them in the tests): * `App\Order` * `App\User` * `SM\SMException` * `Illuminate\Support\Facades\Auth` Let us create a `setUp()` method to get an `Order` instance and log in a `User` for the interactions: ```php namespace Tests\Unit; use Tests\TestCase; use Illuminate\Foundation\Testing\DatabaseMigrations; use Illuminate\Foundation\Testing\DatabaseTransactions; use App\Order; use App\User; use SM\SMException; use Illuminate\Support\Facades\Auth; class StatebleOrderTest extends TestCase { protected $order; protected function setUp() { parent::setup(); $this->order = factory(Order::class)->create(); Auth::login(factory(User::class)->create()); } ``` This will run before every test and give us a fresh `Order` instance to work with. However, to make this possible, we need to define the factory for creating an `Order` in `database/factories/ModelFactory.php`. It can be something like this: ```php // database/factories/ModelFactory.php $factory->define(App\Order::class, function (Faker\Generator $faker) { return [ // all non-nullable fields 'last_state' => 'new' ]; }); ``` Of course you have to fill all your non-nullable fields, otherwise you will see some SQLExceptions when the model is saved. Our first test will make sure we can instantiate a SM on our `Order` model: ```php // tests/StatableOrderTest.php public function testCreation() { $this->assertInstanceOf('SM\StateMachine\StateMachine', $this->order->stateMachine()); } ``` This test will fail if we mess up anything with the dependencies or try to use invalid SM configuration. Our second test will check if the `stateIs()` method returns the current state: ```php // tests/StatableOrderTest.php public function testGetState() { $this->assertEquals('new', $this->order->stateIs()); } ``` Next up, let's test if we can apply a transition: ```php // tests/StatableOrderTest.php public function testTransitionState() { $this->order->transition('process'); // let's refresh the model to see if the state was really persisted $this->order = $this->order->fresh(); $this->assertEquals('processed', $this->order->stateIs()); $this->assertEquals(1,$this->order->history()->count()); $this->order->transition('ship'); $this->order = $this->order->fresh(); $this->assertEquals('shipped', $this->order->stateIs()); $this->assertEquals(2,$this->order->history()->count()); } ``` Here we make two transitions and check if the state has changed and a new history line is added for each transition. Our next test will check the `transitionAllowed()` method: ```php // tests/StatableOrderTest.php public function testTransitionAllowed() { $this->assertTrue($this->order->transitionAllowed('process')); $this->assertFalse($this->order->transitionAllowed('ship')); } ``` The last one will try to apply an invalid transition and expects the SM to throw an `Exception`: ```php // tests/StatableOrderTest.php public function testInvalidTransition() { $this->expectException(SMException::class); $this->order->transition('ship'); } ``` This basically covers the main features, but you can extend it as you like. ## Conclusion We can use the `Statable` trait on any object, we just need to create a _state history_ model drop-in the trait and configure object class. I have created a [Gist](https://gist.github.com/iben12/7e24b695421d92cbe1fec3eb5f32fc94) containing all the code of this tutorial.