この記事は Symfony Advent Calendar 2016 15日目の記事です。

はじめに

ステータスを管理する時などに便利なSymfony/Workflowコンポーネントを紹介したいと思います。

インストール

Composerで楽々インストール出来ます。

$ composer require symfony/workflow

今回のゴール

以下のPull Requestのワークフローのステータス管理を実現したいと思います。

pr-workflow

準備

PullRequestクラスの作成

<?php

class PullRequest {

    private $marking = 'ready';

    public function getMarking()
    {
        return $this->marking;
    }
    public function setMarking($marking)
    {
        $this->marking = $marking;
    }
}

ステータス情報を保持するmarkingプロパティだけを持ったシンプルなクラスになります。
markingプロパティは、自分で任意の名前に変更可能です。
ちなみにGetterSetterが無いと怒られます。

Placeの定義

今回のステータス情報に当たるものは、WorkflowコンポーネントではPlaceと表現されています。

<?php

// use Symfony\Component\Workflow\DefinitionBuilder;

$builder = new DefinitionBuilder();
$builder->addPlaces(['ready', 'wip', 'in-review', 'merged']);

DefinitionBuilder::addPlaces()メソッドで、ready, wip, in-review, mergedの4つのステータスを定義していることになります。

Transitionの定義

どのようにステータス(Place)が遷移していくかをTransitionクラスで定義します。

<?php

// use Symfony\Component\Workflow\Transition;

$builder->addTransition(new Transition('start-work', 'ready', 'wip'));
$builder->addTransition(new Transition('request-review', 'wip', 'in-review'));
$builder->addTransition(new Transition('feedback', 'in-review', 'wip'));
$builder->addTransition(new Transition('merge', 'in-review', 'merged'));

今回の場合は、start-workTransitionは、readyからwipPlaceへの遷移を表現していることになります。

Workflow(StateMachine)インスタンスの作成

Workflow(StateMachine)のインスタンスを作成します。
今回はStateMachineを作成しています。

<?php

// use Symfony\Component\Workflow\DefinitionBuilder;
// use Symfony\Component\Workflow\Transition;
// use Symfony\Component\Workflow\StateMachine;

$builder = new DefinitionBuilder();
$builder->addPlaces(['ready', 'wip', 'in-review', 'merged']);

$builder->addTransition(new Transition('start-work', 'ready', 'wip'));
$builder->addTransition(new Transition('request-review', 'wip', 'in-review'));
$builder->addTransition(new Transition('feedback', 'in-review', 'wip'));
$builder->addTransition(new Transition('merge', 'in-review', 'merged'));
$definition = $builder->build();

$workflow = new StateMachine($definition);

WorkflowとStateMachineの違い

デフォルトでセットされるMarkingStoreが異なるだけで他に違いはありません。
特にMarkingStoreを指定しない場合、StateMachineではSingleMarkingStoreがセットされ、WorkflowではMultipleMarkingStoreがセットされます。

ですので、以下のようにコードを変更しても完全に同じ振る舞いをします。

-$workflow = new StateMachine($definition);
+$marking = new SingleStateMarkingStore();
+$workflow = new StateMachine($definition, $marking);

SingleMarkingStoreとMultipleMarkingStoreの違い

SingleMarkingStoreは名前の通りmarkingに値を1つしか保持出来ませんが、MultipleMarkingStoreは同時に複数保持することが可能です。
今回の場合は1つで事足りると思いますので、SingleMarkingStoreをデフォルトで採用しているStateMachineを利用しています。

実行

それでは実行してみます。

<?php

$pr = new PullRequest();

$workflow->can($pr, 'hoge'); // Thow Symfony\Component\Workflow\Exception\LogicException

$workflow->can($pr, 'merge'); // False

$workflow->apply($pr, 'merge'); // Thow Symfony\Component\Workflow\Exception\LogicException

$workflow->can($pr, 'start-work'); // True

$workflow->apply($pr, 'start-work');
$workflow->can($pr, 'request-review'); // True

$workflow->getEnabledTransitions($pr); // ['feedback', 'merge']

定義したルールに添っていないと例外が投げられたり、Falseが返ってきたりして分かりやすいですね。
ちなみにTwig_Extensionも用意されているので、

{% if workflow_can(pr, 'merge') %}
    <a href="...">Merge</a>
{% endif %}

みたいに書くことも出来ます、便利ですね!

おわりに

ステータス管理にSymfony/Workflowコンポーネントを利用すると「どのように遷移すべきか」「どういった遷移を想定しているか」を明示的に定義することが出来ます。 特に外部サービスのAPIなどを利用している場合は、想定外のレスポンスやステータス遷移を引き起こしてしまう可能性もあるかと思いますので、このように宣言的に設定出来るのは良いプラクティスではないでしょうか。
Symfonyで使う場合は、ワークフローの定義をYamlファイルなどの外部ファイルで簡単に管理出来ますので、一度試してみては如何でしょうか。

補足資料

最終的に実行したコード

Pull Reuest Workflow Sample