definitely246 / state-machine
一个用于PHP的有限状态机。只是随便玩玩。欢迎Pull requests/forks。
Requires
- php: >=5.3.0
Requires (Dev)
- mockery/mockery: 0.9.*
- phpunit/phpunit: ~4.5
This package is not auto-updated.
Last update: 2024-09-14 16:55:35 UTC
README
所有这些……嗯,状态
我们都知道我们都喜欢计算机科学。这是最酷的领域,对吧?我坐在这里回忆起那些辉煌的日子,作为一个年轻的大学生计算机科学专业。我最喜欢做的事情是在白板上绘制有限状态机。我的意思是,艾伦·图灵算什么,有限状态机很棒,对吧?^_^
我将这个状态机写成了我的Laravel 设计模式书中的示例。其他我找到的PHP都让我感到困惑。所以我也不想使用它们。这可能不是写你自己的代码的绝佳理由,但你为什么关心呢?你可以使用这个出色的开源状态机(100%测试覆盖率),这是我写的。好了,不开玩笑了。用这个来创建状态机。以下是它的用法。
安装
使用composer可以安装状态机。
composer require definitely246/state-machine
快速入门示例
状态模式 - 清洁的方法与状态机的替代品
首先让我们考虑状态模式。这是一个非常好的模式来使用。在对象中快速开始状态的一个简单方法是使用StateMachine\Stateful
特质。这将状态模式功能注入到你的类中。这种方法为你的PHP类提供了一个干净的方式来添加状态和上下文。
namespace Light\Stuff; /** * Handle flipSwitch event when light state is on */ class LightOn { public function __construct(\StateMachine\Context $context) { $this->context = $context; } public function flipSwitch() { $this->context->setState(new LightOff($this->context)); $this->context->set('status', 'off'); return 'light is off'; } } /** * Handle flipSwitch event when light state is off */ class LightOff { public function __construct(\StateMachine\Context $context) { $this->context = $context; } public function flipSwitch() { $this->context->setState('LightOn'); $this->context->status = 'on'; return 'light is on'; } } /** * This holds a context object and uses __call() to call our flipSwitch event */ class Light { use \StateMachine\Stateful; protected $context = 'StateMachine\DefaultContext'; protected $state = 'LightOn'; public function status() { return $this->context->status; } }
现在你已经定义了这些类,你就可以开始调用灯的flipSwitch事件。它将使用初始状态来确定这个状态机的所有可用事件。你应该小心地跨板定义相同的事件。你可以实现一个接口来帮助做到这一点。
$light = new Light; // no state yet $light->status $light->flipSwitch(); // 'light is off' $light-flipSwitch(); // 'light is on' $light->status(); // 'on' $light->foobar(); // ERROR! there is no `foobar` method on Light
状态机 - 不同的方法
状态机可以围绕某个上下文对象包装。当调用转换时,处理类会接收事件并根据相应的要求处理它。下面是一个转换处理类,Event1ChangedState1ToState2
。每当从state1触发event1并尝试将状态更改为state2时,它会调用。第二个转换类Event1ChangdState2ToState1
处理当调用event1时将state2转换为state1。
class Event1ChangedState1ToState2 { public function allow($context) { return true; } public function handle($context) { if (!$context->statesChanged) $context->statesChanged = 0; print "state1 -> state2\n"; return $context->statesChanged++; } } class Event1ChangedState2ToState1 { public function allow($context) { return true; } public function handle($context) { print "state2 -> state1\n"; return $context->statesChanged++; } }
接下来我们定义这个有限状态机的转换。
$transitions = [ [ 'event' => 'event1', 'from' => 'state1', 'to' => 'state2', 'start' => true], [ 'event' => 'event1', 'from' => 'state2', 'to' => 'state1' ], ]; $fsm = new StateMachine\FSM($transitions); print $fsm->state() . PHP_EOL; // 'state1' $fsm->event1(); // returns 1, prints 'state1 -> state2' print $fsm->state() . PHP_EOL; // 'state2' $fsm->event1(); // 2, prints 'state2 -> state1' print $fsm->state() . PHP_EOL; // 'state1'
自动售货机示例
想象一下一台允许你购买糖果、小吃、汽水的自动售货机。如果你尝试在不支付的情况下购买一块巧克力,机器不会分发任何东西,对吧?我们可以绘制一个简单的自动售货机的转换。因为我们不是邪恶的,我们会添加一个退款转换。这将允许人们改变主意,取出他们的钱,而不购买任何东西。
注意 这个特定的例子是非确定性的(对于purchase事件有两个结果)。如果你不介意,我也不介意。^_^
转换
要使用StateMachine
,你需要一个转换列表。每个转换都需要一个事件、从状态和到状态。这三样东西构成一个转换。现在我们将这个图转换为事件表,使用上面的fsm图。
$transitions = [ [ 'event' => 'insert', // inserting money 'from' => 'idle', // changes idle state 'to' => 'has money', // to has money state 'start' => true, // this is starting state ], [ 'event' => 'insert', // inserting more 'from' => 'has money', // money is okay 'to' => 'has money', // state does not change ], [ 'event' => 'refund', // refunding when in 'from' => 'has money', // has money state 'to' => 'idle', // sets us back to idle ], [ 'event' => 'purchase', // stops the fsm because 'from' => 'has money', // all items have been 'to' => 'out of stock', // purchased and there is 'stop' => true, // no more idle state ], [ 'event' => 'purchase', // when we make it to this 'from' => 'has money', // transition, we purchase item. 'to' => 'idle', // order matters, see transition above? ], ];
仔细看看上面的转换。我认为我们已经全部找到了。您可以逐步进行。现在我们已经定义了转换,我们需要创建一个使用这些转换的有限状态机。
$machine = new StateMachine\FSM($transitions); // throws StateMachine\Exceptions\TransitionHandlerNotFound
转换事件处理器
我们为这个有限状态机创建了5个转换。默认情况下,每个转换都需要一个处理器类。让我们为我们的第一个事件insert
定义一个处理器类,该事件将状态从空闲转换为有钱。我们需要创建的类名是InsertChangesIdleToHasMoney
。它看起来像这样。
class InsertChangesIdleToHasMoney { public function allow($context) { return true; // allow this state change } public function handle($context) { // do moose stuff here } }
上下文
您可能想知道,这个$context是什么东西?它实际上是一个非常通用的存储对象。如果您愿意,可以在我们的有限状态机上设置自己的上下文对象。上下文传递给所有转换事件。这是状态之间通信更改的一种方式。它是构造函数的第二个参数。
$myCoolerContextObj = new MyCoolerContextObject; $machine = new StateMachine\FSM($transitions, $myCoolerContextObj);
如果您使用Eloquent模型(来自Laravel),您可能会这样做
class MyModel extends \Eloquent { protected $transitions = [ ... ]; public function __construct($attributes = array()) { $this->fsm = new \StateMachine\FSM($this->transitions, $this); } }
对象工厂
但是,这还不是全部。FSM还有一个第三个参数。实际上,让我们看看FSM构造函数的方法签名。您可以看到,您可以将自己的ObjectFactory
对象应用于有限状态机。这个工厂用于创建新的转换处理器对象。如果您想更改处理器类的命名方式,那么您应该覆盖这个工厂。
public function __construct($transitions, $context = null, $factory = '') { $this->whiny = true; $this->stopped = false; $this->context = $context ?: new Context; $this->factory = is_string($factory) ? new ObjectFactory($factory, true) : $factory; $this->transitions = is_array($transitions) ? new Transitions($transitions) : $transitions; $this->state = $this->transitions->startingState(); $this->addTransitionHandlers(); }
如果您传递一个字符串到$factory
,它将使用该字符串作为转换事件类的命名空间。
$context = array(); $machine = new StateMachine\FSM($transitions, $context, '\MyNamespaceToTransitionHandlerEvents'); // throws StateMachine\Exceptions\TransitionHandlerNotFound for \MyNamespaceToTranstitionHandlerEvents\InsertChangesIdleToHasMoney
这使得我们可以将我们的处理器分组到一个单独的命名空间中。现在,StateMachine\Exceptions\TransitionHandlerNotFound
异常应该会告诉我们它找不到\MyNamespaceToTransitionHandlerEvents\InsertChangesIdleToHasMoney
。不错吧?如果您需要更多的控制,例如关闭$strictMode
或更改处理器类的创建方式,则可以使用您自己的ObjectFactory
并将其提供给有限状态机构造函数。
如果您将$strictMode = false
传递给ObjectFactory
,则每当找不到转换处理器类时,对象工厂将返回一个StateMachine\DefaultTransitionHandler
。
如果ObjectFactory
具有strictMode = true
,那么您必须为每个事件转换编写处理器,即使它们只是空的。我建议使用$strictMode = true
,因为它可以快速让您知道需要创建哪些转换事件处理器类,并允许您访问有限状态机的上下文。
抱怨模式
如果您不想为无效的转换事件请求抛出异常,则可以关闭抱怨模式。注意,这会使调试更加困难。
$fsm->whiny = false; $fsm->state() // 'state1' $fsm->canPurchase(); // returns false $fsm->purchase(); // returns false (does not throw CannotTransitionForEvent exception)
取消状态转换
您可以在handle()
方法中使用异常来取消事件转换。
class InsertChangesIdleToHasMoney { public function allow($context) { return true; // allows this transition to run } public function handle($context) { $response = ['some' => 'stuff here']; throw new StateMachine\Exceptions\ShouldNotTransition($response); } }
通过以上对事件转换处理器的更改,我们将得到以下输出
$fsm->state(); // 'idle' $fsm->event3(); // ['some' => 'stuff here'] $fsm->state(); // 'idle' <-- not changed to 'has money' state
触发另一个状态转换
您还可以从另一个状态触发事件。这是复杂的,可能应该避免。但是,如果您发现自己需要在另一个事件内部触发事件,则可以使用TriggerTransitionEvent
。
class InsertChangesHasMoneyToHasMoney { public function allow($context, $coins) { return true; // allows this transition to run } public function handle($context, $coins) { // force the vending machine to refund money... // this ends up calling $fsm->trigger('refund', []); if ($coins < 25) { throw new StateMachine\Exceptions\TriggerTransitionEvent('refund', $args = []); } } }
现在在有钱状态下触发insert实际上导致触发退款
$fsm->state(); // 'has money' $fsm->insert(5); $fsm->state(); // 'idle' <-- the user was refunded
有限状态机停止
您可以在任何时间检查fsm是否已停止。一旦停止,所有触发的事件都将失败。如果抱怨模式为true,则您将得到一个StateMachineIsStopped
异常,否则您将得到一个false。
$fsm->state(); // 'has money' state $fsm->trigger('purchase', ['Pepsi']); // user bought a pepsi $fsm->state(); // 'out of stock' state $fsm->isStopped(); // true $fsm->insert(125); // throws StateMachine\StateMachineIsStopped exception
许可
这是一个MIT许可。这意味着您几乎可以用它来做任何您酷炫的项目。
贡献
如果您想进行更改,请fork此存储库并创建一个pull request。确保您为任何新功能编写了单元测试,并且它们在phpunit中通过。我使用mockery进行模拟。此外,如果您向类添加了职责,您可能需要考虑创建新的类。FSM类已经做了很多。
vendor/bin/phpunit
您走到这里了吗?您已经翻到页面底部了吗?哎呀。您在计算机科学课程中的表现可能比我好很多。再见了,朋友们。祝您今天愉快。^_^