MichaelMoussa / noodle
用PHP编写的有限状态机
Requires
- php: ^7.0
- league/event: ^2.1
Requires (Dev)
- michaelmoussa/php-coverage-checker: ^1.1
- phpunit/phpunit: ^5.2
- squizlabs/php_codesniffer: dev-master as 3.0
This package is not auto-updated.
Last update: 2023-12-04 03:31:13 UTC
README
Noodle是用PHP 7编写的有限状态机
安装
唯一官方支持的安装方法是 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
以更高或更低的优先级来添加到现有的状态转换逻辑,但您可能会发现简单地使用 before
或 after
更容易。
上下文
Noodle 在开始触发事件之前会自动创建一个“上下文”对象,并在整个事件周期中传递。这可以用来将信息从一个监听器传递到另一个监听器。例如,假设您有三个执行各种操作的 before
监听器,然后是一个最终的 before
监听器,在执行状态转换之前记录所有结果。您可以在您的监听器中将操作结果添加到 $context
对象中,然后在记录监听器中读取 $context
将数据写入日志。
请注意,每次状态转换的开始都会创建一个新的上下文,并且它将在事件周期结束时停止存在,因此您必须在事件周期完成之前在事件监听器中使用它。上下文对象是一个简单的 ArrayObject
,应该足够灵活,适用于大多数用例。