该包已被 弃用,不再维护。没有建议的替代包。

用PHP编写的有限状态机

0.1.0 2018-04-07 19:21 UTC

This package is not auto-updated.

Last update: 2023-12-04 03:31:13 UTC


README

Noodle是用PHP 7编写的有限状态机

Build Status Code Coverage Scrutinizer Code Quality Latest Stable Version Latest Unstable Version

安装

唯一官方支持的安装方法是 Composer

composer require michaelmoussa/noodle

使用方法

简单示例

让我们从一个简单的交通灯示例开始。

有三种颜色 - 红色、黄色和绿色。红灯变绿,绿灯变黄,黄灯变红。

这样的系统看起来可能像这样

当前状态 输入 下一个状态
RED CHANGE_COLOR GREEN
GREEN CHANGE_COLOR YELLOW
YELLOW CHANGE_COLOR RED

我们可以用Noodle构建一个状态机来表示这一点

<?php

declare (strict_types = 1);

require_once 'vendor/autoload.php';

use Noodle\Transition\DefaultTransition;
use Noodle\Transition\Table\DefaultTransitionTable;
use Noodle\State\FlyweightState;
use Noodle\Stateful\Stateful;
use Noodle\Stateful\StateMaintainer;
use Noodle\Statemachine\Statemachine;
use Noodle\Transition\Input\FlyweightInput;

/*
 * Create a Transition table to describe the rules for state transitions. For convenience, the
 * DefaultTransition creates transitions based on a pattern, in this
 * case: <CURRENT_STATE> + <INPUT> = <NEXT_STATE>. If you'd like to use a different pattern,
 * you can pass a regex to DefaultTransition::usePattern(...) to substitute your own. The only
 * requirement is that it is a valid regular expression, and that it uses capture groups with
 * the following names: "current_state", "input", "next_state".
 *
 * Alternatively, you could use the following syntax to define transitions, if you wish:
 *     new DefaultTransition(
 *         FlyweightState::named('RED'),
 *         FlyweightInput::named('CHANGE_COLOR'),
 *         FlyweightState::named('GREEN')
 *     )
 */
$table = new DefaultTransitionTable(
    DefaultTransition::new('RED + CHANGE_COLOR = GREEN'),
    DefaultTransition::new('GREEN + CHANGE_COLOR = YELLOW'),
    DefaultTransition::new('YELLOW + CHANGE_COLOR = RED')
);
$statemachine = new Statemachine($table);

/*
 * Any objects that utilize the statemachine must implement the Stateful interface. For
 * convenience, the StateMaintainer trait is available to satisfy the bare minimum
 * requirements of the interface.
 */
class TrafficLight implements Stateful
{
    use StateMaintainer;
}

// Create the stateful object
$trafficLight = new TrafficLight();

/*
 * Give it a default state. In this case, RED. Noodle makes heavy use of Flyweight objects so
 * as to not have to create totally new instances of various States and Inputs throughout
 * your application.
 */
$trafficLight->setCurrentState(FlyweightState::named('RED'));

// Trigger a state transition on $trafficLIght using the CHANGE_COLOR input
$statemachine->trigger(FlyweightInput::named('CHANGE_COLOR'), $trafficLight);

// The light is now green
echo $trafficLight->getCurrentStateName(); // prints "GREEN"

事件

上面的示例相当直接,但并不特别有趣。如果我们需要在灯改变颜色前后做特殊处理怎么办?我们可以使用事件来实现这个逻辑。

Noodle状态机将发出总共十二个事件,您可以监听这些事件。它们按顺序是

  • 特定输入 应用到 特定状态 之前
  • 任何输入 应用到 任何状态 之前
  • 特定输入 应用到 任何状态 之前
  • 任何输入 应用到 特定状态 之前
  • 任何输入 应用到 任何状态
  • 特定输入 应用到 特定状态 之后
  • 任何输入 应用到 任何状态 之后
  • 特定输入 应用到 任何状态 之后
  • 任何输入 应用到 特定状态 之后

假设,每次灯光改变颜色之前,你都想大声宣布。这里有一种方法可以设置它

<?php

use League\Event\EventInterface;
use Noodle\Listener\InvokableListener;
use Noodle\State\State;
use Noodle\Stateful\Stateful;
use Noodle\Transition\Input\Input;

class LightChangingAnnouncement extends InvokableListener
{
    public function __invoke(
        EventInterface $event,
        Stateful $object,
        \ArrayObject $context,
        Input $input,
        State $nextState
    ) {
        echo sprintf('Hey everyone, the light is about to turn %s!', $nextState->getName());
    }
}

/*
 * $statemachine is from the previous example. Note the FlyweightState::any() here. This is
 * a "wildcard" state that, for the purposes of the statemachine event system, will match any
 * current state. This is useful in cases where you don't care what the state is, and you
 * know that you want to execute the event every time there's a state change.
 */
$statemachine->before(
    FlyweightInput::named('CHANGE_COLOR'),
    FlyweightState::any(),
    new LightChangingAnnouncement()
);

现在,在灯光改变颜色之前,它会宣布将要变成的颜色。你可以使用 ->after(...) 监听其他事件,可选的第四个 int 参数表示与其他监听器的优先级。

Noodle使用流行的 league/event 库作为其事件系统,并提供了 InvokableListener 抽象类以方便使用,但只要你实现 League\Event\ListenerInterface,就可以使用自己的监听器。

失败和状态变化

如果在状态转换之前触发的任何监听器通过调用 $event->stopPropagation() 方法表示它失败了,Noodle 将执行 Noodle\Listeners\ReportsTransitionFailures 监听器,该监听器会抛出 StateTransitionFailed 异常。这是 Noodle 使用的默认错误处理机制。如果您想以不同的方式处理错误,可以将您自己的监听器作为可选的第 2 个参数传递给 Statemachine 构造函数,并且它将被使用。当然,如果您在您的监听器中抛出异常而不是停止传播,Noodle 将允许该异常传播到您的应用程序。

默认情况下,状态转换由 Noodle\Listener\ChangesState 监听器处理,该监听器简单地调用您的 Stateful 对象的 setCurrentState(...) 方法。这个监听器是由 Noodle 发出的唯一 on 事件触发的。您通常不需要对其进行任何更改,这些更改不能在其他事件监听器中完成,但如果您确实需要,可以将您自己的监听器作为 Statemachine 构造函数的第 3 个参数传递,Noodle 将使用该监听器而不是默认的 ChangesState 监听器来更新对象的状态。当然,您也可以通过将您自己的监听器添加到 on 以更高或更低的优先级来添加到现有的状态转换逻辑,但您可能会发现简单地使用 beforeafter 更容易。

上下文

Noodle 在开始触发事件之前会自动创建一个“上下文”对象,并在整个事件周期中传递。这可以用来将信息从一个监听器传递到另一个监听器。例如,假设您有三个执行各种操作的 before 监听器,然后是一个最终的 before 监听器,在执行状态转换之前记录所有结果。您可以在您的监听器中将操作结果添加到 $context 对象中,然后在记录监听器中读取 $context 将数据写入日志。

请注意,每次状态转换的开始都会创建一个新的上下文,并且它将在事件周期结束时停止存在,因此您必须在事件周期完成之前在事件监听器中使用它。上下文对象是一个简单的 ArrayObject,应该足够灵活,适用于大多数用例。