Skip to content
Paris edited this page Apr 10, 2020 · 15 revisions

yii2-statemachine

A modern State Machine for the Yii2 framework

Version Compatibility

This documentation applies to version v1.x.x

Features

  1. Stateful model attributes with persistence
  2. Reusable transition commands (command pattern)
  3. Transition Journal so you can always tell who did what and when
  4. StateTimeouts - Automatic transitions based on timeouts
  5. Support for RBAC systems - Define who can see and trigger an event
  6. Visual Representation of your StateMachine
  7. Simple and powerful StateMachine definition via XML
  8. Sequenced perplexed State Machines per model

Who should use it?

This FSM best serves medium to Enterprise level codebases where the databases and business logic can become so complicated that FSMs are a necessity to maintain healthy levels of sanity. However, this shouldn't discourage smaller projects with a desire to grow fast or have a fancy FSM.

First steps in a glance

  1. Installation and migrations
  2. Configure your StateMachine application component. (or as many as you need)
  3. Install the behaviour in your model
  4. Support for User Roles
  5. Using our FSM
  6. Timeouts (cli - cronjob)

Installation

Recommended via composer

composer require ptheofan/yii2-statemachine

StateMachine Database Tables (Migrations)

This will create 2 tables sm_journal and sm_timeout. They will be used automatically by the StateMachines. If you need to modify them you need to extend the existing implementations and instruct your StateMachines to use them by adjusting the configuration of $modelJournal and $modelTimeout

./yii migrate --migrationPath='@vendor/ptheofan/yii2-statemachine/migrations/'

Example

Lets assume we want to wrap our users in a state machine. TODO: SCREENSHOT. We will do so using the example-account.xml graph as provided.

1. Create the state machine component configuration.

if you have multiple state machines you have to do this multiple times. One time for each state machine as each state machine is its very own Component. So, in your configuration file in the components section add the following.

  'smUserAccountStatus' => [
      'class' => 'ptheofan\statemachine\StateMachine',
      'schemaSource' => '@vendor/ptheofan/yii2-statemachine/example-account.xml',
      'name' => 'account',
  ],

2. Add the behavior to your model.

Go to your database model (ie. User extends ActiveRecord) and add the behavior. If you have multiple attributes controlled by state machines in the same class you have to add the behavior multiple times. One time per controlled attribute.

  /**
   * @return array
   */
  public function behaviors()
  {
      return [
          'status' => [
              'class' => StateMachineBehavior::className(),
              'sm' => Yii::$app->smUserAccountStatus,         // The component name we used in the config section.
              'attr' => '_status',                            // The physical attribute
              'virtAttr' => 'status',                         // The attribute used for control point.
          ],
      ];
  }

3. Roles Based schema.

For v2.x.x READ HERE In this scenario we are also using roles. Roles are simple strings declared in the xml file for each action (optional). Since most commonly you will be needing roles, let's add a function in the User model that will inform the state machine of the current user's role. The name, the function, even when the function is can be adjusted from the StateMachine configuration (config section). See the StateMachine class for details.

  /**
   * @param User $user
   * @return string
   */
  public function getUserRole($user)
  {
      if (!$user) {
          return self::ROLE_GUEST;
      }

      if ($user->role === User::ROLE_ADMIN) {
          return self::ROLE_ADMIN;
      }

      if ($user->role === User::ROLE_SYSTEM) {
          return self::ROLE_SYSTEM;
      }

      if ($this->hasProperty('created_by') && $this->created_by === $this->created_by) {
          return self::ROLE_OWNER;
      }

      return self::ROLE_USER;
  }

4. Ready to use

Finally the configuration of the state machine is ready! Here's a simple example with some cases

  // Creating a model for the first time - StateMachine will automatically introduce the model to the schema
  $tx = User::getDb()->beginTransaction();
  $user = new User();
  $user->email = 'test@example.com';
  $user->setPassword('123');
  $user->save();
  $tx->commit();
  
  // A very simple way of triggering events based on the state we would like to move the object to. This will
  // work only if there's 1 event that can lead to the desired state.
  $tx = User::getDb()->beginTransaction();
  /** @var User $user */
  $user = User::find()->andWhere(['email' => 'test@example.com'])->one();
  $user->status = 'verified';     // This is the state we want to set
  $tx->commit();
  
  // A more formal way of doing the same - using the Event's label
  $tx = User::getDb()->beginTransaction();
  /** @var User $user */
  $user = User::find()->andWhere(['email' => 'test@example.com'])->one();
  $user->status->trigger('verify');     // This is the label of the event we trigger
  $tx->commit();

5. Timeouts (cli)

Add the following to your configuration (if using Advanced Template that would be at console/config/main.php)

    'controllerMap' => [
         'sm' => [
            'class' => 'ptheofan\statemachine\console\StateMachineController',
        ],
     ],

and call it in the command line using the following

./yii sm/timeouts

I recommend scheduling crontab to run ./yii sm/timeouts every minute. Do not worry for performance. Timeouts command is very lightweight. As with all cronjobs, make sure you keep logs so can double check all work fine in the long run.

* * * * * /usr/bin/php path/to/yii sm/timeouts