definitely246/state-machine

一个用于PHP的有限状态机。只是随便玩玩。欢迎Pull requests/forks。

v1.0.2 2015-03-30 01:09 UTC

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'

自动售货机示例

想象一下一台允许你购买糖果、小吃、汽水的自动售货机。如果你尝试在不支付的情况下购买一块巧克力,机器不会分发任何东西,对吧?我们可以绘制一个简单的自动售货机的转换。因为我们不是邪恶的,我们会添加一个退款转换。这将允许人们改变主意,取出他们的钱,而不购买任何东西。

a vending machine finite state machine

注意 这个特定的例子是非确定性的(对于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

您走到这里了吗?您已经翻到页面底部了吗?哎呀。您在计算机科学课程中的表现可能比我好很多。再见了,朋友们。祝您今天愉快。^_^