-
-
Save kimulisiraj/c858f2beccfd67e98e083fcfcc1f4be9 to your computer and use it in GitHub Desktop.
Revisions
-
iben12 renamed this gist
Sep 13, 2017 . 1 changed file with 0 additions and 0 deletions.There are no files selected for viewing
File renamed without changes. -
iben12 renamed this gist
Sep 13, 2017 . 1 changed file with 0 additions and 0 deletions.There are no files selected for viewing
File renamed without changes. -
iben12 revised this gist
Sep 13, 2017 . 1 changed file with 1 addition and 1 deletion.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -4,7 +4,7 @@ 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. -
iben12 revised this gist
Sep 12, 2017 . 1 changed file with 1 addition and 1 deletion.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -9,7 +9,7 @@ Practically this means you define each state that the SM can be in and the possi 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, which is a Laravel service provider for the [winzou/state-machine](https://github.com/winzou/state-machine) package. So let's `require` that: ``` -
iben12 revised this gist
Sep 12, 2017 . 1 changed file with 13 additions and 12 deletions.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -220,7 +220,7 @@ First will be the `stateIs()` method. public function stateIs() { return $this->stateMachine()->getState(); } ``` Now we can call this method on the model to get the current state: @@ -233,7 +233,7 @@ Next we need a method for applying a transition, let it be `transition()`: public function transition($transition) { return $this->stateMachine()->apply($transition); } ``` so we can use it like: @@ -247,7 +247,7 @@ Also we have a method on the SM that helps determine if a transition can be appl public function transitionAllowed($transition) { return $this->stateMachine()->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: @@ -307,7 +307,7 @@ if ($order->transitionAllowed('process') { $order->transition('process'); $order->save(); } else { // handle 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. @@ -352,14 +352,15 @@ class StateHistroyManager } } ``` 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 by calling `addHistoryLine`. So, from now on we don't even have to bother with saving the model after a state change. Let's make the method: ```php // app/Traits/Statable.php public function addHistoryLine(array $transitionData) { $this->save(); $transitionData['user_id'] = auth()->id(); return $this->history()->create($transitionData); } ``` @@ -382,7 +383,7 @@ Route::get('/order/{order}/{transition}', function (App\Order $order, $transitio return $order->history()->get(); }); ``` The response will be something like the following `json` if the order state was `new`: ```json [ { @@ -396,10 +397,10 @@ The response will be something like the following `json`: } ] ``` ## Taking it a step 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 `Eloquent\Model`s only 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 @@ -420,10 +421,10 @@ public function history() { /** @var \Eloquent $model */ $model = app(self::HISTORY_MODEL['name']); return $model->where(self::HISTORY_MODEL['foreign_key'], $this->{self::PRIMARY_KEY}); // maybe use scope here } ``` For this we have to configure the `HISTORY_MODEL['foreign_key']` and `PRIMARY_KEY` on non-eloquent objects which will be used to handle the history. The `addHistoryLine` method would look like this: ```php -
iben12 revised this gist
Sep 12, 2017 . 1 changed file with 1 addition and 1 deletion.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -212,7 +212,7 @@ trait Statable } } ``` 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->stateMachine()->getState()`), however we will implement convenience methods to access some interactions with short syntax. First will be the `stateIs()` method. ```php -
iben12 revised this gist
Sep 12, 2017 . 1 changed file with 1 addition and 1 deletion.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -40,7 +40,7 @@ where `$object` is the entity whose state we want to manage and `$graph` is the ## 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. This also helps us to clarify what want to implement. 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: -
iben12 revised this gist
Sep 12, 2017 . 1 changed file with 1 addition and 1 deletion.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -33,7 +33,7 @@ $ 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(SM\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. -
iben12 renamed this gist
Sep 12, 2017 . 1 changed file with 0 additions and 0 deletions.There are no files selected for viewing
File renamed without changes. -
iben12 renamed this gist
Sep 12, 2017 . 1 changed file with 0 additions and 0 deletions.There are no files selected for viewing
File renamed without changes. -
iben12 revised this gist
Sep 12, 2017 . 1 changed file with 3 additions and 1 deletion.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -1,4 +1,6 @@ # Implementing State Machine On Eloquent Model* \* Update (12.09.2017): I have improved the trait so that it can be used with objects other than Eloquent Models. 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. -
iben12 revised this gist
Sep 12, 2017 . 1 changed file with 0 additions and 2 deletions.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -557,5 +557,3 @@ 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. -
iben12 revised this gist
Sep 12, 2017 . 4 changed files with 177 additions and 74 deletions.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -186,7 +186,7 @@ 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 @@ -201,7 +201,7 @@ trait Statable */ protected $stateMachine; public function stateMachine() { if (!$this->stateMachine) { $this->stateMachine = app(FactoryInterface::class)->get($this, self::SM_CONFIG); @@ -212,32 +212,34 @@ trait Statable ``` 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 @@ -248,14 +250,14 @@ public function transitionAllowed($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. @@ -272,7 +274,9 @@ 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 @@ -284,7 +288,7 @@ Now we can manage the state of the model like this: $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 @@ -298,7 +302,7 @@ Or alternatively we can check if the transition can be applied first: $order = App\Order::first(); if ($order->transitionAllowed('process') { $order->transition('process'); $order->save(); } else { // hanle rejection @@ -339,19 +343,25 @@ class StateHistroyManager $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: @@ -363,7 +373,7 @@ Route::get('/order/{order}/{transition}', function (App\Order $order, $transitio // make sure you have autheticated user by route middleware or Auth check try { $order->transition($transition); } catch(Exception $e) { return abort(500, $e->getMessage()); } @@ -384,6 +394,56 @@ The response will be something like the following `json`: } ] ``` ## 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: ``` @@ -432,36 +492,41 @@ 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()); } ``` @@ -485,11 +550,12 @@ The last one will try to apply an invalid transition and expects the SM to throw { $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. This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -9,9 +9,10 @@ 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 } This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -1,9 +1,10 @@ <?php namespace App\Traits; use Illuminate\Database\Eloquent\Model; use SM\Factory\FactoryInterface; use SM\StateMachine\StateMachine; trait Statable { @@ -12,31 +13,61 @@ trait Statable */ protected $stateMachine; 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}); } 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); } public function stateIs() { return $this->StateMachine()->getState(); } public function transition($transition) { return $this->stateMachine()->apply($transition); } public function transitionAllowed($transition) { return $this->StateMachine()->can($transition); } /** * @return StateMachine */ public function stateMachine() { if (!$this->stateMachine) { $this->stateMachine = app(FactoryInterface::class)->get($this, self::SM_CONFIG); } return $this->stateMachine; } public function isEloquent() { return $this instanceof Model; } } This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -23,24 +23,29 @@ protected function setUp() public function testCreation() { $this->assertInstanceOf('SM\StateMachine\StateMachine', $this->order->stateMachine()); } public function testGetState() { $this->assertEquals('new', $this->order->stateIs()); } 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()); } @@ -54,6 +59,6 @@ public function testInvalidTransition() { $this->expectException(SMException::class); $this->order->transition('ship'); } } -
iben12 revised this gist
Sep 11, 2017 . 1 changed file with 1 addition and 1 deletion.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -2,7 +2,7 @@ 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. -
iben12 revised this gist
Feb 3, 2017 . 1 changed file with 5 additions and 6 deletions.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -389,7 +389,7 @@ 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` @@ -421,13 +421,12 @@ This will run before every test and give us a fresh `Order` instance to work wit $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 @@ -444,12 +443,12 @@ Our second test will check if the `state()` method returns the current state: ```php // tests/StatableOrderTest.php public function testGetState() { $this->assertEquals('new', $this->order->state()); } ``` Next up, let's test if we can apply a transition: ```php // tests/StatableOrderTest.php -
iben12 revised this gist
Feb 3, 2017 . 1 changed file with 22 additions and 22 deletions.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -38,7 +38,7 @@ where `$object` is the entity whose state we want to manage and `$graph` is the ## 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: @@ -113,11 +113,11 @@ class AddLastStateToOrdersTable extends Migration } } ``` 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 @@ -131,7 +131,7 @@ class CreateOrderStatesTable extends Migration { Schema::create('order_states', function (Blueprint $table) { $table->increments('id'); $table->integer('order_id'); $table->string('transition'); $table->string('to'); $table->integer('user_id'); @@ -172,7 +172,7 @@ class OrderState extends Model ``` 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: @@ -210,7 +210,7 @@ trait Statable } } ``` 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 `state()` method. This we can use to get the current state, or apply a state transition depending on the provided argument of the method: ```php @@ -234,7 +234,7 @@ or initiate a transition by using the name of the transition as the first argume ``` $order->state('process'); ``` NOTE: Yes, some might say, this is a bad practice. Actually I got the idea from javascript, where it is common that a method is a getter if no arguments given, and a setter if there is one. Feel free to create two separate methods for getting and setting if that makes you feel better (like `currentState()` and `applyTransition()`). Next 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: @@ -246,7 +246,7 @@ 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 @@ -324,7 +324,7 @@ protected $listen = [ ], ]; ``` Let's create that class: ```php // app/Listeners/StateHistoryManager.php @@ -349,17 +349,17 @@ class StateHistroyManager } } ``` Since the `Event` contains the SM instance and that contains the model instance our job is easy. We get the model, create a history relation on it that is filled up with the data and save the model. So, from now on we don't even have to bother with saving the model after a state change. 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. ## 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 { @@ -374,13 +374,13 @@ 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" } ] ``` @@ -415,7 +415,7 @@ class StatebleOrderTest extends TestCase 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 @@ -449,7 +449,7 @@ Our second test will check if the `state()` method returns the current state: $this->assertEquals('new', $this->order->state()); } ``` Next up, let's test if we can init a transition: ```php // tests/StatableOrderTest.php -
iben12 revised this gist
Feb 3, 2017 . 1 changed file with 496 additions and 0 deletions.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,496 @@ # 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 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, named `graphA`, provided by the package. The config returns an array of _graphs_. You can get rid of the predefined _graph_ or leave it as is for further 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 too: ``` $ php artisan make:model OrderState -m ``` This will create a model class in `app/OrderState` 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->string('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 _Statble_ 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 `getStateMachine` 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 getStateMachine() { 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 easily. First will be the `state()` method. This we can use to get the current state, or apply a state transition depending on the provided argument of the method: ```php // app/Traits/Statable.php public function state($transition = null) { if ($transition) { return $this->getStateMachine()->apply($transition); } else { return $this->getStateMachine()->getState(); } } ``` Now we can call this method on the model either without argument to get the current state: ```php $currentState = $order->state(); ``` or initiate a transition by using the name of the transition as the first argument: ``` $order->state('process'); ``` NOTE: Yes, some might say, this is a bad practice. Actually I got the idea from javascript, where it is common that a method is a getter if no arguments given, and a setter if there is one. Next 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 is we need 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 public function history() { return $this->hasMany(self::HISTORY_MODEL); } ``` As you can see the related model class will be configured on the model itself by the `HISTORY_MODEL` constant. 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 = '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->state('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->state('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: ```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->history()->create([ "transition" => $event->getTransition(), "to" => $sm->getState(), "user_id" => auth()->id() ]); $model->save(); } } ``` Since the `Event` contains the SM instance and that contains the model instance our job is easy. We get the model, create a history relation on it that is filled up with the data and save the model. 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 (ex. in the routes). ## 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 Route::get('/order/{order}/{transition}', function (App\Order $order, $transition) { // the URL is like /order/8/process // make sure you have autheticated user by route middleware or Auth check try { $order->state($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" } ] ``` ## 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 use an IDE you can import these as you write): * `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. To make this work, 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 [ 'total' => 100, // other non-nullable fields 'last_state' => 'new' ]; }); ``` Of course you have to fill all your non-nullable fields to be able to create the model, 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->getStateMachine()); } ``` 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 `state()` method returns the current state: ```php // tests/StatableOrderTest.php public function testCreation() { $this->assertEquals('new', $this->order->state()); } ``` Next up, let's test if we can init a transition and if the transition creates a new history line: ```php // tests/StatableOrderTest.php public function testApplyState() { $this->order->state('process'); $this->assertEquals('processed', $this->order->state()); $this->assertEquals(1,$this->order->history()->count()); $this->order->state('ship'); $this->assertEquals('shipped', $this->order->state()); $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->state('ship'); } ``` This basically covers the main features, but you can extend it as you like. ## Conclusion We can use the `Statable` trait on any other model, we just need to create a _state history_ model and drop-in the trait into the model class. I have created a [Gist](https://gist.github.com/iben12/7e24b695421d92cbe1fec3eb5f32fc94) containing all the code of this tutorial. -
iben12 revised this gist
Feb 3, 2017 . 1 changed file with 1 addition and 1 deletion.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -44,7 +44,7 @@ public function testApplyState() $this->assertEquals(2,$this->order->history()->count()); } public function testTransitionAllowed() { $this->assertTrue($this->order->transitionAllowed('process')); $this->assertFalse($this->order->transitionAllowed('ship')); -
iben12 revised this gist
Feb 3, 2017 . 1 changed file with 3 additions and 6 deletions.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -1,5 +1,4 @@ <?php namespace Tests\Unit; @@ -18,10 +17,8 @@ class StatebleOrderTest extends TestCase protected function setUp() { parent::setup(); $this->order = factory(Order::class)->create(); Auth::login(factory(User::class)->create()); } public function testCreation() @@ -59,4 +56,4 @@ public function testInvalidTransition() $this->order->state('ship'); } } -
iben12 revised this gist
Feb 3, 2017 . 1 changed file with 62 additions and 0 deletions.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,62 @@ <?php // tests/Unit/StatableOrderTest.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([ "user_id" => factory(User::class)->create()->id ]); Auth::login(User::find($this->order->user->id)); } public function testCreation() { $this->assertInstanceOf('SM\StateMachine\StateMachine', $this->order->getStateMachine()); } public function testGetState() { $this->assertEquals('new', $this->order->state()); } public function testApplyState() { $this->order->state('process'); $this->assertEquals('processed', $this->order->state()); $this->assertEquals(1,$this->order->history()->count()); $this->order->state('ship'); $this->assertEquals('shipped', $this->order->state()); $this->assertEquals(2,$this->order->history()->count()); } public function testCanTransitState() { $this->assertTrue($this->order->transitionAllowed('process')); $this->assertFalse($this->order->transitionAllowed('ship')); } public function testInvalidTransition() { $this->expectException(SMException::class); $this->order->state('ship'); } } -
iben12 revised this gist
Feb 3, 2017 . 7 changed files with 7 additions and 0 deletions.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -1,3 +1,4 @@ <?php // app/Order.php namespace App; This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -1,3 +1,4 @@ <?php // app/OrderState.php namespace App; This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -1,3 +1,4 @@ <?php // app/Traits/Statable.php namespace App\Traits This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -1,3 +1,4 @@ <?php // app/Listeners/StateHistoryManager.php namespace App\Listeners; This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -1,3 +1,4 @@ <?php // database/migrations/yyyy_mm_dd_hhmmss_add_last_state_to_orders_table.php use Illuminate\Support\Facades\Schema; This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -1,3 +1,4 @@ <?php // database/migrations/yyyy_mm_dd_hhmmss_create_order_states_table.php use Illuminate\Support\Facades\Schema; This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -1,3 +1,4 @@ <?php // config/state_machine.php return [ -
iben12 created this gist
Feb 3, 2017 .There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,16 @@ // app/Order.php namespace App; use Traits\Statable; use Illuminate\Database\Eloquent\Model; class Order extends Model { use Statable; const HISTORY_MODEL = '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 } This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,18 @@ // 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'); } } This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,41 @@ // app/Traits/Statable.php namespace App\Traits use SM\Factory\FactoryInterface; trait Statable { /** * @var StateMachine $stateMachine */ protected $stateMachine; public function getStateMachine() { if (!$this->stateMachine) { $this->stateMachine = app(FactoryInterface::class)->get($this, self::SM_CONFIG); } return $this->stateMachine; } public function state($transition = null) { if ($transition) { return $this->getStateMachine()->apply($transition); } else { return $this->getStateMachine()->getState(); } } public function transitionAllowed($transition) { return $this->getStateMachine()->can($transition); } public function history() { return $this->hasMany(self::HISTORY_MODEL); } } This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,22 @@ // 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->history()->create([ "transition" => $event->getTransition(), "to" => $sm->getState(), "user_id" => auth()->id() ]); $model->save(); } } This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,22 @@ // 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'); }); } public function down() { Schema::table('oders', function($table) { $table->dropColumn('last_state'); }); } } This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,25 @@ // 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->string('order_id'); $table->string('transition'); $table->string('to'); $table->integer('user_id'); $table->timestamps(); }); } public function down() { Schema::dropIfExists('order_states'); } } This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,40 @@ // 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' ] ] ], //... ]