読者です 読者をやめる 読者になる 読者になる

あざらし備忘録。

渋谷ではたらく音ゲー大好きウェッブエンジニアがいろいろ思った事やった事を書いていくブログです

PHPのステートマシンFiniteを触ってみた[PHP][Finite][StateMachine]

今回はステートマシンをPHPで扱えるFiniteというライブラリを触ってみたので備忘録として。

ステートマシンとはっていうところからFiniteの簡単な使い方までを軽くまとめてみようかなと思います。

ステートマシンとは

ステートマシンとは、すごく簡単に言うと状態遷移を管理するもので、ある状態Aからある状態Bへの遷移が可能かどうかを判定したり実際に遷移を実行したりします。

細かいところはステートマシンやオートマトンなどで調べると出てくるので興味があればmm

雄弁に語れるほどわかりきれてないので今の理解だと上記程度の感じですw

アプリケーションでステートマシンを用いるようにすると次のようなメリットがあります。

  • ステータスをもつオブジェクトの状態遷移の定義を閉じ込めておける
  • 定義通りにしか状態遷移できなくなるためフローが確立される
  • 定義に反した状態遷移を行おうとした際には例外等が起きることで適切に処理できる

状態遷移は往々にして管理が複雑になって、プロダクト内の色々な箇所で判定を都度都度行うようになったり、都度都度行うせいでバグの温床になったり、状態遷移のフローが変わった時には一斉に判定している部分を修正しなければならなかったりと、厄介な目に合うことが多いです。

例えば、ある作業を行うチケットの状態遷移を考えてみます。

f:id:shiro_goma:20150222041357p:plain

●が初期状態、◎を終了状態としています。

さっと考えてみただけでもこんな感じに状態遷移が出てきそうに思います。

他にもチケットをクローズ状態を追加したり、クローズからリオープンしたり、どんな状態からでも強制的に終了状態に持っていける却下状態を追加したり。考え方によっては色々な状態遷移フローが出来上がります。

「この状態だったり矢印だったりをいい感じに取り扱いたい!」そんなときに是非使っていきたいのがステートマシンです!

Finiteとは

上記で説明したステートマシンのPHP実装です。

Finite, A PHP5.3+ Finite State Machine | Finite

PHP5.3以上なら使えるので導入の敷居は低いと思います!

インストール

composerで入ります!

stableとしては1.0.3がタグ付けされていますが、現在開発中?のmasterは1.1になるようで、結構仕様が変わっていきそうなので(StateMachineのコンストラクタ引数が変わっていたり)、今回はdev-masterを指定してインストールしました。

"require": {
    "yohang/finite": "dev-master"
}

使い方

公式のサンプルコードに則って説明していこうと思います。

Finite/basic-graph.php at master · yohang/Finite · GitHub

<?php

require_once __DIR__ . '/../vendor/autoload.php';


/**
 * Class Document
 * 状態を持つオブジェクトのサンプルとしてドキュメントが使われています。
 * 管理したい状態を持つオブジェクトはFinite\StatefulInterfaceをimplementsします。
 *
 * StatefulInterfaceはgetFiniteState()とsetFiniteState()を実装することを要求しているので、
 * 状態管理したいプロパティをset、getできるようしておいてあげます。
 */
// Implement your document class
class Document implements Finite\StatefulInterface
{
    private $state;

    public function getFiniteState()
    {
        return $this->state;
    }

    public function setFiniteState($state)
    {
        $this->state = $state;
    }
}

// Configure your graph
$document     = new Document;

/**
 * ドキュメントインスタンスを渡してステートマシンのインスタンスを作成します。
 * この渡したものに対してステートマシンは状態管理を行います。
 */
$stateMachine = new Finite\StateMachine\StateMachine($document);

/**
 * ローダーを作成します。
 * ここで状態遷移のルールを決めていきます。
 * 下記の例だとドキュメントに対してのルールを定めていっています。
 * ローダーには以下の情報を設定することができます。
 *   * class
 *     * 設定したいクラス名をいれます。
 *   * states
 *     * 状態の設定をします。
 *     * type
 *       * 初期状態、通常状態、終了状態の3つから選択します。
 *       * デフォルトは通常状態です。
 *     * properties
 *       * 状態に持たせたい設定情報があれば、連想配列形式で自由に持たせる事ができます。
 *   * transitions
 *     * states間の遷移を設定します。
 *     * どの状態からどの状態には遷移可能、というのを記述します。
 *   *
 *
 * "draft"というstateは「初期状態であり、削除可能かつ編集可能」という設定になっています。
 */
$loader       = new Finite\Loader\ArrayLoader(array(
    'class'  => 'Document',
    'states'  => array(
        'draft' => array(
            'type'       => Finite\State\StateInterface::TYPE_INITIAL,
            'properties' => array('deletable' => true, 'editable' => true, 'comment' => '下書き'),
        ),
        'proposed' => array(
            'type'       => Finite\State\StateInterface::TYPE_NORMAL,
            'properties' => array('comment' => '提出済'),
        ),
        'accepted' => array(
            'type'       => Finite\State\StateInterface::TYPE_FINAL,
            'properties' => array('printable' => true),
        )
    ),
    'transitions' => array(
        'propose' => array('from' => array('draft'), 'to' => 'proposed'),
        'accept'  => array('from' => array('proposed'), 'to' => 'accepted'),
        'reject'  => array('from' => array('proposed'), 'to' => 'draft'),
    ),
));


/**
 * 上記で作成したローダーで、ステートマシンに設定をロードします。
 * これでステートマシンがコンストラクタ引数にて受け取ったインスタンスに対してどのように状態を管理していくかを
 * 理解することができるようになります。
 */
$loader->load($stateMachine);

/**
 * ステートマシンの初期化を行います。
 * ここでローダーのルールに従って状態をもつインスタンスの初期化を行います。
 * 同時に、ステートマシンも「現時点での状態」を知ります。
 * 上記の例だと初期化するとtypeがINITIALである"draft"がセットされることになります。
 */
$stateMachine->initialize();


// Working with workflow

// Current state

var_dump($stateMachine->getCurrentState()->getName()); // 初期状態なので"draft"が返ります。
/**
string(5) "draft"
 */
var_dump($stateMachine->getCurrentState()->getProperties()); // 初期状態のプロパティが返ります。
/**
array(2) {
["deletable"]=>
bool(true)
["editable"]=>
bool(true)
["comment"]=>
string(9) "下書き"
} */
var_dump($stateMachine->getCurrentState()->has('deletable')); // draftが'deletable'プロパティを持つかを返します。持っているのでtrue
/**
 * bool(true)
 */
var_dump($stateMachine->getCurrentState()->has('printable')); // draftが'printable'プロパティを持つかを返します。持っていないのでfalse
/**
 * bool(false)
 */

var_dump($stateMachine->getCurrentState()->get('comment')); // draftがもつ'comment'プロパティを返します。'下書き'が返ります
/**
 * string(9) "下書き"
 */

var_dump($stateMachine->getCurrentState()->get('printable')); // draftがもつ'printable'プロパティを返します。持っていない場合はNULL
/**
 * NULL
 */

// Available transitions
var_dump($stateMachine->getCurrentState()->getTransitions()); // draftの持っているtransitionsを返します。
/**
array(1) {
    [0]=>
  string(7) "propose"
}
 */

var_dump($stateMachine->can('propose')); // 'propose'トランジションを適用可能かを判定します。draftはproposeトランジションの開始状態なのでtrue
/**
 * bool(true)
 */

var_dump($stateMachine->can('accept')); // 'accept'トランジションを適用可能かを判定します。draftはproposeトランジションの開始状態ではないのでfalse
/**
 * bool(false)
 */

// Apply transitions
try {
    $stateMachine->apply('accept'); // 適用可能でないトランジションを適用しようとするとStateExceptionが投げられます。
} catch (\Finite\Exception\StateException $e) {
    echo $e->getMessage(), "\n";
    /**
     * The "accept" transition can not be applied to the "draft" state of object "Document" with graph "default".
     */
}

// Applying a transition
$stateMachine->apply('propose'); // 適用すると状態が遷移します。

var_dump($stateMachine->getCurrentState()->getName()); // 状態が遷移したのでdraftからproposedに変化しています。
/**
 * string(8) "proposed"
 */
var_dump($document->getFiniteState()); // 状態が遷移したのでdraftからproposedに変化しています。
/**
 * string(8) "proposed"
 */
var_dump($stateMachine->getCurrentState()->getProperties()); // 取得できるプロパティもproposedのものに変化しています。
/**
array(1) {
["comment"]=>
string(9) "提出済"
}
 */

この様に、現在の状態の情報を管理したり、管理対象の状態を変更可能かを判定したり、管理対象の状態を変更したり、といったことを上手くラップして操れるようになっています。

便利ですね!

今回は基本的な動かし方についてチュートリアルをやってみました。

他にも状態遷移の直前、直後にコールバックを噛ませて任意のコードを実行するように設定を加えることも可能だったりして、なかなか考えられているなぁと感じました。 あとは今回はfiniteStateというプロパティに強制されていましたが、任意の、しかも複数個の状態を管理しようとすることもできます。

また、DIコンテナを用いることも想定されていて、セットするひな形みたいなのも用意されています。 PimpleやSymfony2のサービスコンテナへのファクトリーの追加用のクラスもありました。

Finite/src/Finite/Factory at master · yohang/Finite · GitHub

ただ、Pimpleは1系想定のものに見えるので、2系対応にしたい!という場合等、ここにはないもので扱いたい場合はだとAbstractFactoryをextendsしたりFactoryInterfaceをimplementsしたりして対応するものを自作する流れになりそうです。

この辺りの応用編についても書く機会があったら書きたいなぁと思います!

とにかくはじめはライトに使うとしてもステートマシンはとても便利で手堅くアプリケーションを作ることができるようになると思うので、ぜひ使っていってみてください!

今回のブログ用に触ってみたリポジトリは以下になります。

gomachan46/finiteStudy · GitHub