Skip to content

Instantly share code, notes, and snippets.

@kimulisiraj
Forked from iben12/1_Laravel_state-machine.md
Created November 10, 2017 12:00
Show Gist options
  • Save kimulisiraj/c858f2beccfd67e98e083fcfcc1f4be9 to your computer and use it in GitHub Desktop.
Save kimulisiraj/c858f2beccfd67e98e083fcfcc1f4be9 to your computer and use it in GitHub Desktop.

Revisions

  1. @iben12 iben12 renamed this gist Sep 13, 2017. 1 changed file with 0 additions and 0 deletions.
  2. @iben12 iben12 renamed this gist Sep 13, 2017. 1 changed file with 0 additions and 0 deletions.
    File renamed without changes.
  3. @iben12 iben12 revised this gist Sep 13, 2017. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion _Laravel_state-machine.md
    Original 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.
    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.

  4. @iben12 iben12 revised this gist Sep 12, 2017. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion _Laravel_state-machine.md
    Original 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, that is a Laravel service provider for the [winzou/state-machine](https://github.com/winzou/state-machine) package.
    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:
    ```
  5. @iben12 iben12 revised this gist Sep 12, 2017. 1 changed file with 13 additions and 12 deletions.
    25 changes: 13 additions & 12 deletions _Laravel_state-machine.md
    Original file line number Diff line number Diff line change
    @@ -220,7 +220,7 @@ First will be the `stateIs()` method.

    public function stateIs()
    {
    return $this->getStateMachine()->getState();
    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->getStateMachine()->apply($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->getStateMachine()->can($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 {
    // hanle rejection
    // 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. 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:
    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();

    $this->save();
    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`:
    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 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`.
    ## 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`.
    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});
    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 save the history.
    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
  6. @iben12 iben12 revised this gist Sep 12, 2017. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion _Laravel_state-machine.md
    Original 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->getStateMachine()->getState()`), however we will implement convenient methods to access some interactions with short syntax.
    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
  7. @iben12 iben12 revised this gist Sep 12, 2017. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion _Laravel_state-machine.md
    Original 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. 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.
    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:

  8. @iben12 iben12 revised this gist Sep 12, 2017. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion _Laravel_state-machine.md
    Original 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(FactoryInterface::class)->get($object,$graph);
    $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.

  9. @iben12 iben12 renamed this gist Sep 12, 2017. 1 changed file with 0 additions and 0 deletions.
    File renamed without changes.
  10. @iben12 iben12 renamed this gist Sep 12, 2017. 1 changed file with 0 additions and 0 deletions.
    File renamed without changes.
  11. @iben12 iben12 revised this gist Sep 12, 2017. 1 changed file with 3 additions and 1 deletion.
    4 changes: 3 additions & 1 deletion 1_tutorial.md
    Original file line number Diff line number Diff line change
    @@ -1,4 +1,6 @@
    # Implementing State Machine On Eloquent Model
    # 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.

  12. @iben12 iben12 revised this gist Sep 12, 2017. 1 changed file with 0 additions and 2 deletions.
    2 changes: 0 additions & 2 deletions 1_tutorial.md
    Original 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.

    I have created a [Gist](https://gist.github.com/iben12/7e24b695421d92cbe1fec3eb5f32fc94) containing all the code of this tutorial.
  13. @iben12 iben12 revised this gist Sep 12, 2017. 4 changed files with 177 additions and 74 deletions.
    160 changes: 113 additions & 47 deletions 1_tutorial.md
    Original 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 `getStateMachine` method for this purpose:
    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 getStateMachine()
    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 `state()` method. This we can use to get the current state, or apply a state transition depending on the provided argument of the method:
    First will be the `stateIs()` method.
    ```php
    // app/Traits/Statable.php

    public function state($transition = null)
    public function stateIs()
    {
    if ($transition) {
    return $this->getStateMachine()->apply($transition);
    }
    else {
    return $this->getStateMachine()->getState();
    }
    return $this->getStateMachine()->getState();
    }
    ```
    Now we can call this method on the model either without argument to get the current state:
    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
    $currentState = $order->state();
    // app/Traits/Statable.php

    public function transition($transition)
    {
    return $this->getStateMachine()->apply($transition);
    }
    ```
    or initiate a transition by using the name of the transition as the first argument:
    so we can use it like:
    ```
    $order->state('process');
    $order->transition('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:
    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
    // app/Traits/Statable.php

    public function history()
    {
    return $this->hasMany(self::HISTORY_MODEL);
    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.
    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 = 'App\OrderState'; // the related model to store the history
    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->state('process');
    $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->state('process');
    $order->transition('process');
    $order->save();
    } else {
    // hanle rejection
    @@ -339,19 +343,25 @@ class StateHistroyManager
    $sm = $event->getStateMachine();
    $model = $sm->getObject();

    $model->history()->create([
    $model->addHistoryLine([
    "transition" => $event->getTransition(),
    "to" => $sm->getState(),
    "user_id" => auth()->id()
    "to" => $sm->getState()
    ]);

    $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. So, from now on we don't even have to bother with saving the model after a state change.
    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();

    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.
    $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->state($transition);
    $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->getStateMachine());
    }
    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 `state()` method returns the current state:
    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->state());
    }
    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 testApplyState()
    public function testTransitionState()
    {
    $this->order->state('process');
    $this->order->transition('process');

    $this->assertEquals('processed', $this->order->state());
    // 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->state('ship');
    $this->order->transition('ship');

    $this->order = $this->order->fresh();

    $this->assertEquals('shipped', $this->order->state());
    $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->state('ship');
    $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 other model, we just need to create a _state history_ model and drop-in the trait into the model class.
    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.
    5 changes: 3 additions & 2 deletions Order.php
    Original file line number Diff line number Diff line change
    @@ -9,9 +9,10 @@
    class Order extends Model
    {
    use Statable;
    const HISTORY_MODEL = 'App\OrderState'; // the related model to store the history
    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

    }
    65 changes: 48 additions & 17 deletions Statable.php
    Original file line number Diff line number Diff line change
    @@ -1,9 +1,10 @@
    <?php
    // app/Traits/Statable.php

    namespace App\Traits
    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 getStateMachine()
    {
    if (!$this->stateMachine) {
    $this->stateMachine = app(FactoryInterface::class)->get($this, self::SM_CONFIG);
    public function history() {
    if ($this->isEloquent()) {
    return $this->hasMany(self::HISTORY_MODEL['name']);
    }
    return $this->stateMachine;

    /** @var \Eloquent $model */
    $model = app(self::HISTORY_MODEL['name']);
    return $model->where(self::HISTORY_MODEL['foreign_key'], $this->{self::PRIMARY_KEY});

    }

    public function state($transition = null)
    public function addHistoryLine(array $transitionData)
    {
    if ($transition) {
    return $this->getStateMachine()->apply($transition);
    }
    else {
    return $this->getStateMachine()->getState();
    $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->getStateMachine()->can($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 history()
    public function isEloquent()
    {
    return $this->hasMany(self::HISTORY_MODEL);
    return $this instanceof Model;
    }
    }
    }
    21 changes: 13 additions & 8 deletions StatableOrderTest.php
    Original 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->getStateMachine());
    $this->assertInstanceOf('SM\StateMachine\StateMachine', $this->order->stateMachine());
    }

    public function testGetState()
    {
    $this->assertEquals('new', $this->order->state());
    $this->assertEquals('new', $this->order->stateIs());
    }

    public function testApplyState()
    public function testTransitionState()
    {
    $this->order->state('process');
    $this->order->transition('process');

    $this->assertEquals('processed', $this->order->state());
    // 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->state('ship');
    $this->order->transition('ship');

    $this->order = $this->order->fresh();

    $this->assertEquals('shipped', $this->order->state());
    $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->state('ship');
    $this->order->transition('ship');
    }
    }
  14. @iben12 iben12 revised this gist Sep 11, 2017. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion 1_tutorial.md
    Original 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 state in which the SM should be after the transition.
    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.

  15. @iben12 iben12 revised this gist Feb 3, 2017. 1 changed file with 5 additions and 6 deletions.
    11 changes: 5 additions & 6 deletions 1_tutorial.md
    Original 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 use an IDE you can import these as you write):
    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 [
    'total' => 100,
    // other non-nullable fields
    // all 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.
    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 testCreation()
    public function testGetState()
    {
    $this->assertEquals('new', $this->order->state());
    }
    ```
    Next up, let's test if we can init a transition:
    Next up, let's test if we can apply a transition:
    ```php
    // tests/StatableOrderTest.php

  16. @iben12 iben12 revised this gist Feb 3, 2017. 1 changed file with 22 additions and 22 deletions.
    44 changes: 22 additions & 22 deletions 1_tutorial.md
    Original 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, 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.
    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 too:
    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` and a migration. Edit the migration first:
    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->string('order_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 _Statble_ Trait
    ## 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 easily.
    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.
    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 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:
    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:
    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.
    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 (ex. in the routes).
    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)
    { // the URL is like /order/8/process

    {
    // 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"
    "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. 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:
    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 and if the transition creates a new history line:
    Next up, let's test if we can init a transition:
    ```php
    // tests/StatableOrderTest.php

  17. @iben12 iben12 revised this gist Feb 3, 2017. 1 changed file with 496 additions and 0 deletions.
    496 changes: 496 additions & 0 deletions 1_tutorial.md
    Original 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.
  18. @iben12 iben12 revised this gist Feb 3, 2017. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion StatableOrderTest.php
    Original 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 testCanTransitState()
    public function testTransitionAllowed()
    {
    $this->assertTrue($this->order->transitionAllowed('process'));
    $this->assertFalse($this->order->transitionAllowed('ship'));
  19. @iben12 iben12 revised this gist Feb 3, 2017. 1 changed file with 3 additions and 6 deletions.
    9 changes: 3 additions & 6 deletions StatableOrderTest.php
    Original file line number Diff line number Diff line change
    @@ -1,5 +1,4 @@
    <?php
    // tests/Unit/StatableOrderTest.php

    namespace Tests\Unit;

    @@ -18,10 +17,8 @@ class StatebleOrderTest extends TestCase
    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));
    $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');
    }
    }
    }
  20. @iben12 iben12 revised this gist Feb 3, 2017. 1 changed file with 62 additions and 0 deletions.
    62 changes: 62 additions & 0 deletions StatableOrderTest.php
    Original 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');
    }
    }
  21. @iben12 iben12 revised this gist Feb 3, 2017. 7 changed files with 7 additions and 0 deletions.
    1 change: 1 addition & 0 deletions Order.php
    Original file line number Diff line number Diff line change
    @@ -1,3 +1,4 @@
    <?php
    // app/Order.php

    namespace App;
    1 change: 1 addition & 0 deletions OrderState.php
    Original file line number Diff line number Diff line change
    @@ -1,3 +1,4 @@
    <?php
    // app/OrderState.php

    namespace App;
    1 change: 1 addition & 0 deletions Statable.php
    Original file line number Diff line number Diff line change
    @@ -1,3 +1,4 @@
    <?php
    // app/Traits/Statable.php

    namespace App\Traits
    1 change: 1 addition & 0 deletions StateHistoryManager.php
    Original file line number Diff line number Diff line change
    @@ -1,3 +1,4 @@
    <?php
    // app/Listeners/StateHistoryManager.php

    namespace App\Listeners;
    1 change: 1 addition & 0 deletions add_last_state_to_orders_table.php
    Original 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;
    1 change: 1 addition & 0 deletions create_order_states_table.php
    Original 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;
    1 change: 1 addition & 0 deletions state-machine.php
    Original file line number Diff line number Diff line change
    @@ -1,3 +1,4 @@
    <?php
    // config/state_machine.php

    return [
  22. @iben12 iben12 created this gist Feb 3, 2017.
    16 changes: 16 additions & 0 deletions Order.php
    Original 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

    }
    18 changes: 18 additions & 0 deletions OrderState.php
    Original 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');
    }
    }
    41 changes: 41 additions & 0 deletions Statable.php
    Original 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);
    }
    }
    22 changes: 22 additions & 0 deletions StateHistoryManager.php
    Original 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();
    }
    }
    22 changes: 22 additions & 0 deletions add_last_state_to_orders_table.php
    Original 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');
    });
    }
    }
    25 changes: 25 additions & 0 deletions create_order_states_table.php
    Original 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');
    }
    }
    40 changes: 40 additions & 0 deletions state-machine.php
    Original 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'
    ]
    ]
    ],

    //...
    ]