rolylvreijdenberger/izzum-statemachine

一个优秀的statemachine库,php版本 >= 5.3,与您的领域模型完美集成。

4.0.0 2016-06-10 07:38 UTC

README

Build Status Total Downloads Latest Stable Version Code Coverage Scrutinizer Code Quality License

一个优秀的、可扩展且灵活的statemachine库,适用于php版本 >= 5.3,包括php 7。

一个 有限状态机 是一个系统行为的模型,该系统由有限数量的状态、状态之间的转换以及状态和转换的守卫和转换逻辑组成。

请在此处查看 变更日志

请在此处查看 Amsterdam phpmeetup 的演示文稿

关于

这是一个经过验证的企业级、完全单元测试且高质量的状态机。它具有使用不同后端(postgres、redis、sqlite、mongodb、mysql、session 或内存)存储状态数据和转换历史的能力,以及配置状态机(使用状态、转换和转换逻辑,支持yaml、json、xml、sql、redis 或 mongodb)的能力。

它将通过操作现有领域模型(如'Order'、'Customer'等)而无缝地与现有领域模型协同工作,而不是必须创建包含状态机逻辑的新领域模型(这也是可能的)。示例、广泛的(内联)文档和单元测试将使其易于设置和启动。

欢迎在 1zzumvx7zVHv3AdWXQ1XUuNKyQonx7uHM 上进行比特币捐赠。

从 3.y.z 版本升级到 php 7 的 4.y.z 版本升级路径

  • 在数据库/yml/xml/json配置中使用 False Rule、True Rule 或 Null Command 的升级定义:使用 'FalseRule'、'TrueRule'、'NullCommand'
  • 在代码中使用 False Rule、True Rule 或 Null Command 的引用升级:使用 'FalseRule'、'TrueRule'、'NullCommand'

示例演示

以下代码示例将引导您使用statemachine,并使您熟悉与statemachine交互的不同方式。

创建一个statemachine

实体ID和机器名称为您的statemachine提供唯一的定义。它们共同构成了在您的应用程序中识别statemachine的最简单方式。实体ID很可能是您应用程序中领域模型的唯一ID或主键。机器名称是一个名称,用于区分一种机器类型与另一种机器类型(例如'订单机'、'客户机')。将它们一起存储在标识符对象中。

上下文为您的statemachine的操作提供操作上下文。上下文使用标识符来唯一识别一个statemachine。上下文还提供将状态存储在所选后端的方法,并提供了通过可选构造函数参数(稍后解释)操作特定领域模型的方法。上下文至少需要一个标识符。Statemachine需要一个上下文来定义其操作的环境和对象。

//retrieve the id somewhere from your application (form, database, domain model, user input etc)
$identifier = new Identifier('198442', 'order');//the identifier for your machine
$context = new Context($identifier);//other parameters for Context will be explained later
$machine = new StateMachine($context);//create the statemachine

添加状态和转换

状态可以从一个状态转换到另一个状态。状态机必须恰好有1个'初始'类型的状态,0个或多个'普通'类型的状态和0个或多个'最终'类型的状态。定义您的状态(带有可选类型,默认类型为'普通'),并使用它们来定义转换。

转换可以由一个'事件'字符串触发,该字符串可以作为Transition构造函数的第三个参数设置。事件名称默认为转换名称,其形式为<状态从>_to_<状态到>

当定义了转换后,将它们添加到状态机中。转换和状态可以在它们的构造函数中接受更多参数(稍后解释)。

$new = new State('new', State::TYPE_INITIAL);//there must be 1 initial state
$action = new State('action');//a normal state
$done = new State('done', State::TYPE_FINAL);//one of potentially many final states
$machine->addTransition(new Transition($new, $action, 'go'));//add a transition between states that is triggered by an event
$machine->addTransition(new Transition($action, $done, 'finish'));
//result: 3 states and 2 transitions defined on the statemachine

可以通过加载器(稍后详细介绍)添加转换,这样您就可以在配置文件或选择的持久化后端中定义状态机。这为您在定义状态机(并保持它们在版本控制下)时提供了更大的灵活性。

获取状态机信息

上下文提供有关机器的上下文信息,因此它包含该机器的元数据:机器名称、实体ID(都在标识符中)、持久化适配器、用于存储您的状态和转换数据(由适配器指定的后端)以及一个领域模型构建器,它作为工厂用于创建您的领域模型,状态机的逻辑将使用该实体ID执行操作(稍后解释)

$context = $machine->getContext();
echo $context->getPersistenceAdapter();//echo works because of Memory::__toString()
# Memory
echo $context->getEntityId();//get the id for your domain model (entity)
# 198442
echo $context->getMachine();//get the name of the statemachine
# order
echo count($machine->getStates());//get the defined states directly from the machine
# 3
echo count($machine->getTransitions());//get the defined transitions directly from the machine
# 2

获取有关状态的信息

当机器初始化时,当前状态将是初始状态。状态可以直接设置,也可以在使用时从持久化后端检索(稍后解释)。

$state = $machine->getCurrentState();
echo $state->getName();
# new
echo $state->getType();
# initial
echo $machine->getInitialState();//echo works because of State::__toString()
# new
foreach($machine->getStates() as $state) {
    echo $state->getName();
}
# new, action, done

添加正则表达式状态,该状态可以展开为多个转换

正则表达式状态以其状态名称为正则表达式。当在转换中将正则表达式状态添加到状态机时,它将展开为状态机中所有匹配正则表达式的已知状态的转换。这允许您快速设置大量转换。它可以用于'从'状态以及'到'状态。正则表达式状态名称应以前缀'regex:'或'not-regex:'开头,用于否定正则表达式。

//action, or any state ending with 'ew'
$regex = new State('regex:/action|.*ew$/', State::TYPE_REGEX);
$pause = new State('pause');
$machine->addTransition(new Transition($regex, $pause), 'pause');
//new->pause, action->pause

获取有关转换的信息

转换可以通过事件字符串、名称(它是它们的'从'状态和'到'状态的串联)或匿名地通过尝试从当前状态执行转换来触发。状态机可以查询允许的转换以及它们如何在机器当前所在的当前状态中被允许。

//the current state 'new', has a transition that can be triggered by the 'go' event
echo $machine->hasEvent('go'); 
# true
//current state is 'new' and 'finish' is only a valid event for the 'action' state
echo $machine->hasEvent('finish');
# false
echo $machine->canHandle('go');//this will check the guard logic for the 'go' event (explanation later)
# true
//transitions have a name derived from their 'from' and 'to' states
echo $machine->canTransition('new_to_action');
# true
//not in the 'action' state
echo $machine->canTransition('action_to_done');
# false
//check the state itself for a transition
echo $machine->getCurrentState()->hasTransition('new_to_action');
# true
foreach ($machine->getTransitions() as $transition) {
    echo $transition->getName() . ":" . $transition->getEvent(); 
}
# new_to_action:go, action_to_done:finish, new_to_pause:pause, action_to_pause:pause

interactive example

执行特定转换

可以通过调用StateMachine::handle('<event>')(通过定义的事件名称处理转换)、通过调用Statemachine::<event>()(使用事件名称作为方法调用)或通过调用StateMachine::transition('<name>')(通过名称调用转换)来执行从当前状态到特定转换。所有方法在成功执行转换时都将返回true。如果未指定转换的事件名称,则事件将默认为转换名称,该名称始终采用<状态从>_to_<状态到>的格式。因此,您还可以调用Statemachine::<transition-name>()(如果未指定事件名称)。

//the next three lines all produce the same result and are different ways to perform the transition to 'action' from the state 'new'
$machine->handle('go');//handle the 'go' trigger/event to transition to 'action' state
$machine->go();//use the trigger name directly as a method on the machine
$machine->transition('new_to_action');//transition by name <from>_to_<to>

//suppose we are in the 'action' state, the next lines produce the same result: a transition to 'done'
$machine->handle('finish');//transition to 'done' state via 'finish' trigger/event
$machine->finish();
$machine->transition('action_to_done');

执行匿名/通用转换

可以通过尝试运行状态机当前所在的当前状态中允许的第一个转换来机会性地执行转换。转换按它们添加到机器的顺序尝试。可以通过使用'守卫'来允许或禁止转换:特定于该转换的代码片段,可以检查业务规则(稍后解释)。

echo $machine->run();//perform the first transition from the current state that can run
# true
//perform as many transitions as the machine allows: 
//each transition will go to the 'to' state and will try the next transition from there,
//until it is in a final state or transitions are not possible for that new current state.
echo $machine->runToCompletion();//suppose we started in 'new', then 2 transitions will be made
# 2

使用EntityBuilder构建机器的领域模型

应该执行转换逻辑以执行有用的工作。状态机应在或与您的应用程序的领域对象一起操作。EntityBuilder的子类应创建您的应用程序特定的领域模型,该模型将在每个转换操作中使用。可以重写EntityBuilder::build(Identifier $identifier):*方法以返回任何可以由状态机使用的领域对象(例如:订单或客户),该对象由实体ID标识,该ID可能是您应用程序中该对象的键。领域模型将用于转换的守卫条件和转换逻辑(包括退出和进入逻辑)。可以将EntityBuilder子类的一个实例作为第三个构造参数传递给Context对象,该对象将注入状态机。

如果没有使用特定的EntityBuilder,则默认为返回Identifier对象的EntityBuilder。这很有用,因为默认情况下,您将获得传递给转换守卫和逻辑的Identifier对象,您可以操作Identifier来获取实体ID并使用它来对您的领域问题进行操作。

$identifier = new Identifier('198442', 'order-machine');
$builder = new OrderBuilder();
$context = new Context($identifier, $builder);
$machine = new StateMachine($context);
//the builder class for an Order would look like this:
class OrderBuilder extends EntityBuilder{
  protected function build(Identifier $identifier) {
    return new Order($identifier->getEntityId());
  }
}

转换上的守卫条件

守卫条件是动态评估的布尔表达式,它允许或禁止转换。守卫不应该有副作用,并且应该仅计算布尔结果。

如果没有在转换上指定守卫,则默认允许转换。守卫可以操作由EntityBuilder返回的领域模型。

有多种方法可以在转换上设置守卫条件

  • 可调用对象:闭包/匿名方法、实例方法和静态方法,返回布尔值。这很容易使用,并且可能从领域模型中解耦。缺点是所有代码都应该始终在内存中定义。
  • 规则业务规则是izzum/rules/Rule实例的完全限定类名,这些实例将在其构造函数中接受领域模型(通过EntityBuilder),并且应该实现返回布尔值的Rule::applies()方法,该布尔值在可能与注入规则中的领域模型交互后返回。这是最正式且最强大的守卫,因为它以非侵入性和松耦合的方式在领域模型上操作。此外,代码(可能是昂贵的,例如:访问数据库或网络服务)仅在需要时实例化和使用,而与其他所有方法不同,这些方法应该始终在内存中完全可用。
  • 事件处理器:在Context中通过EntityBuilder对指定的领域对象进行调用。这很灵活且方便,因为您可以在可由状态机访问的领域模型上定义事件处理器。
  • 钩子:通过在子类化状态机时重写特定的方法StateMachine::_onCheckCanTransition()来使用。这适合您的应用程序领域,并且与其他方法相比提供的灵活性较少,因为您需要“切换”到转换以采取特定的操作。
  • 作为事件分发器的钩子:通过子类化状态机,您可以实现自己的事件处理/分发库。

守卫条件 1. 使用可调用对象:闭包、静态方法、实例方法

PHP 中可调用对象(callable)有多种形式。在下一个示例中,使用了一个闭包或匿名函数来通过操作其作用域内的任何上下文变量以及自动提供的 $entity 和 $event 参数来评估布尔表达式。$entity 是状态机上下文(通过 EntityBuilder)返回的领域模型。当转换由事件触发时,事件才被设置。守卫可以操作 $entity(如果没有使用 Builder,则默认为 Identifier)来计算布尔结果。

通常,所有可调用对象都将传递 2 个参数 $entity 和 $event,并且应该具有以下方法签名:[static] public function <name>($entity, $event = null): boolean

如果你在 PHP 脚本中定义转换,那么你比通过配置文件或持久化后端加载配置时具有更多选项。在加载转换配置时,只能使用 \fully\qualified\Class::staticMethod 形式进行可调用对象,因为不能在配置中将闭包定义为一个字符串。

请查看 examples/inheritance 中的示例,了解如何将实例方法用作可调用对象。有关 izzum 状态机中可调用对象的所有可能实现的详细信息,请参阅 tests/izzum/statemachine/TransitionTest::shouldAcceptMultipleCallableTypes

$forbidden = new State('forbidden');
$closure = function($entity, $event){return false;};
$transition = new Transition($new, $forbidden, 'thoushaltnotpass', null, null, $closure);
// or: $transition->setGuardCallable($closure);
$machine->addTransition($transition);
echo $machine->hasEvent('thoushaltnotpass');
# true
echo $machine->handle('thoushaltnotpass');//transition will not be made
# false
echo $machine->transition('new_to_forbidden');
# false
echo $machine->getCurrentState();//still in the same state
# new

守卫条件 2. 使用业务规则

通过创建一个 Rule 类(\izzum\rules\Rule 的子类)并将完全限定的类名作为字符串设置在转换上,使用业务规则。仅在检查转换时动态实例化 Rule 类,并且将领域模型(通过 EntityBuilder 由上下文提供)注入到其构造函数中。Rule 应该有一个 Rule::applies() 方法,该方法将返回一个布尔值,该值将通过查询领域模型或任何其他数据源(例如:服务、API、数据库等)进行计算。

提供了 FalseRule 规则作为示例。你应该为你的问题域编写自己的特定规则。请参阅 examples/trafficlight 以了解使用规则和具有 EntityBuilder 的领域对象的实现。

规则永远不应该有副作用,并且只能返回布尔值。

可以通过指定多个完全限定的规则类名(用逗号 , 分隔)将多个规则链接在一起(使用逻辑合取:and)。

测试得到了简化,因为你可以将 测试替身(模拟/存根)注入到你的 Rule 中。

$forbidden = new State('forbidden');
$rule = '\izzum\rules\FalseRule';
$transition = new Transition($new, $forbidden, 'thoushaltnotpass', $rule);
// or: $transition->setRuleName($rule);
$machine->addTransition($transition);
echo $machine->hasEvent('thoushaltnotpass');
# true
echo $machine->handle('thoushaltnotpass');//transition will not be made
# false
echo $machine->transition('new_to_forbidden');
#> false
echo $machine->getCurrentState();//still in the same state
# new

Rule 子类将在其构造函数中注入你的领域对象(由 EntityBuilder 创建)并查询该对象以确定业务规则是否适用。

class IsAllowedToShip extends Rule {
  public function __construct(Order $order) { $this->order = $order;}
  protected function _applies() { return $this->order->isPaid(); }
}

使用规则作为守卫的配置应通过提供完全限定的类名来完成。PHP 应用程序必须能够通过自动加载(包括文件的包装器)找到该类。

$rule = '\izzum\rules\IsAllowedToShip';
$transition = new Transition($action, new State('shipping'), 'ship', $rule);

使用规则作为守卫的优点是,你的领域模型和状态机之间没有耦合,这使得你的应用程序代码更加干净和易于测试。

守卫条件 3. 使用事件处理器

由EntityBuilder子类返回的类可以实现事件处理器:当发生转换时触发的回调。这既适用于守卫,也适用于转换逻辑。请注意,该类可以是状态机的子类、状态机的客户端类或现有的领域模型。该类可以实现预定义的事件处理器public function onCheckCanTransition($identifier, $transition, $event):boolean,它接收一个标识符对象和转换对象以及一个可选的事件(如果转换是通过$statemachine->handle('event')触发的)。它必须返回一个布尔值。转换对象可以用来根据转换名称或'from'和'to'状态执行某些操作。该方法必须返回一个布尔值。

class MyEventHandlingClass {
  public function onCheckCanTransition($identifier, $transition, $event) { 
    echo "checking transition (" . $identifier->getEntityId() . ") " . $transition->getName() ' for event: ' . $event;
    //normally, you would put your guard logic here...
    return true;
  }
}

存在一个特殊的EntityBuilder子类:ModelBuilder,它始终返回构造函数中注入的模型。如果您正在实现事件处理器并想在状态机中使用事件处理类,这很有用。

$builder = new ModelBuilder(new MyEventHandlingClass());
$context = new Context($identifier, $builder);
$statemachine = new StateMachine($context);
//load the machine here and assume we are in the 'new' state in which a transition can be triggered by the 'go' event
$statemachine->handle('go');
# checking transition (198442) new_to_action for event: go

将事件处理器作为守卫的缺点是,与使用规则作为守卫相比,状态机和处理类之间的耦合更加紧密。

守卫条件 4. 使用钩子/重写方法

通过子类化状态机,您可以实现一个作为守卫调用的钩子:protected function _onCheckCanTransition(Transition $transition, $event = null):boolean。参见examples/inheritance以获取使用状态机子类、钩子/重写方法的示例。优点是您不需要EntityBuilder,可以轻松重写所需的方法。缺点是您的模型(需要状态的领域模型)现在通过继承与您的状态机紧密耦合。

守卫条件 5. 使用带事件分派器的钩子

子类化的一个重大优点是,它允许您使用钩子添加自己的逻辑以满足需求。如果您想响应状态机的事件,添加将事件监听器添加到状态机的可能性,并在子类中添加事件分派器,您就可以以任何您想要的方式响应状态机的操作(例如:使用转换数据发送特定事件)。添加一个公开方法到状态机以添加事件监听器,并从子类的钩子中分发事件,如果需要在事件中使用转换、状态和状态机数据。一个好的事件分派器是Symfony EventDispatcher组件,您可以使用它来返回布尔评估参数以允许或禁止转换。

逻辑动作:状态进入动作、状态退出动作和转换动作

状态退出逻辑、转换逻辑和状态进入逻辑都提供将自定义领域逻辑关联到从'from'状态到'to'状态的转换阶段的方法。如果没有在转换或状态上指定逻辑处理器,则默认不会发生任何操作。逻辑处理器可以操作由EntityBuilder返回的领域模型。

尝试执行转换时存在4个不同的阶段

  • 检查守卫以确定转换是否允许,如果是则
  • 执行状态退出逻辑:与'from'状态关联,独立于转换所去的状态。这始终在离开状态作为转换的一部分时执行,无论转换去向何方。
  • 执行转换逻辑:与转换本身关联,具有特定的'from'和'to'状态,并进入新状态。这始终在状态已退出且在进入新状态之前执行。
  • 执行状态进入逻辑:与'to'状态关联,独立于转换来自的状态。这始终在进入状态作为转换的一部分时执行,无论转换来自何方。

与使用守卫的逻辑类似,退出和退出逻辑可以通过 callablescommands(见命令设计模式)、事件处理程序钩子 来执行。

有多种方法在转换上设置逻辑处理程序。

  • callables:闭包/匿名方法、实例方法和静态方法。这很容易使用,并且可能与领域模型解耦。缺点是所有代码都必须在定义转换时定义并驻留在内存中。
  • commands命令(设计模式) 封装可重用的逻辑,并指定为 izzum/command/Command 类的完全限定类名,这些类在其构造函数中应接受领域模型(通过 EntityBuilder),并且应实现 Command::execute() 方法,该方法可以 潜在地与命令中注入的领域模型交互。这是使用处理逻辑最正式和最强大的方式,因为它以非侵入性和松耦合的方式操作领域模型。此外,代码(可能是昂贵的运行,例如:访问数据库或网络服务)仅在需要时实例化和使用,而与其他所有方法不同,这些方法应该始终完全驻留在内存中。
  • 事件处理器:在Context中通过EntityBuilder对指定的领域对象进行调用。这很灵活且方便,因为您可以在可由状态机访问的领域模型上定义事件处理器。
  • hooks:通过在继承状态机本身时重写特定方法(StateMachine::_onExitState()StateMachine::_onTransition()StateMachine::_onEnterState())来使用。这适用于您的应用程序领域,并且比其他方法提供更少的灵活性,因为您需要根据转换进行“切换”以采取特定操作。
  • 作为事件调度程序的hooks:通过继承状态机,您可以在提供的钩子中实现自己的事件处理/调度库。

逻辑操作 1. 通用转换流程(callables、事件、hooks)

有关 callables、事件和 hooks 的一般实现细节,请参阅示例部分中的守卫条件。

通用转换逻辑序列如下

  • 退出
    • 钩子: _onExitState($transition)
    • 事件处理程序: $entity->onExitState($identifier, $transition)
    • 执行退出命令
    • callables: $callable($entity)
  • 转换
    • 钩子: _onTransition($transition)
    • 事件处理程序: $entity->onTransition($identifier, $transition)
    • 执行转换命令
    • callables: $callable($entity)
  • 退出
    • 事件处理程序: $entity->onEnterState($identifier, $transition)
    • 执行进入命令
    • callables: $callable($entity)
    • 钩子: _onEnterState($transition)

逻辑操作 2. 命令

通过创建单独的命令类(\izzum\command\Command 的子类)并将它的完全限定类名作为字符串设置在转换或状态上,来使用命令。

命令类仅在需要执行逻辑时动态实例化,并将领域模型(通过 Context 通过 EntityBuilder 提供)注入其构造函数中。命令应有一个 Command::execute() 方法,该方法将通过 潜在地操作领域模型 或其他数据源(例如:服务、API、数据库等)来执行逻辑。提供 NullCommand 命令作为示例。您应该编写适用于您问题域的特定命令。请参阅 examples/trafficlight 了解使用命令和具有 EntityBuilder 的领域对象的实现。

可以通过指定多个完全限定命令类名(用逗号分隔)将多个命令链接在一起,作为一个 组合。请注意,如果您有一个转换,其中包含 3 个命令,最后一个抛出异常,则可能需要再次执行转换,从而再次执行前两个。

命令子类将在其构造函数中注入您的领域对象(由 EntityBuilder 制作),并可以使用该对象执行其逻辑。

配置带有命令的转换或状态时,应提供完整的类名。PHP应用程序必须能够通过自动加载(包括文件包装器)找到该类。

与规则不同,命令很可能有副作用,因为它们的行为会影响您的程序。

使用命令进行转换逻辑的优点是,您的领域模型与状态机之间没有耦合,这使得您的应用程序代码更加简洁且易于测试。测试变得简单,因为您可以在命令中注入测试替身(模拟/存根)。

traffic light example

traffic light state diagram

Class OrderDelivery extends Command {
public function __construct(Order $order) { $this->order = $order;}
  protected function _execute() { $this->order->deliver(); }
}
$command = '\izzum\command\OrderDelivery';
//assume we are using the rule from the example
$transition = new Transition($action, new State('shipping'), 'ship', $rule, $command);

使用持久化适配器存储状态

持久化适配器提供了一种抽象方法,将状态数据和转换历史写入所选的持久化后端,因此您的状态机可以在多个连续的PHP进程中使用,因为它会记住它处于哪种状态。

默认情况下,izzum为以下基于SQL的后端提供了持久化适配器:postgresqlmysqlsqlite(它们都通过php PDO库工作),redis键/值数据库(NoSQL)和基于文档的mongoDB(NoSQL)。这些适配器都支持它们各自PHP驱动程序的全套功能。它们使用已知稳定的PHP模块(PDO、redis、mongo)实现,更多信息可以在类和php.net上的phpdocs中找到。

半持久化适配器是PHP会话适配器,非持久化适配器是内存适配器(默认)。

适配器是persistence\Adapter的子类,您将其实例作为上下文对象的第三个参数提供。提供的持久化适配器具有完全定义的后端结构,示例可以在assets/<backend>中找到。如果您使用这些后端适配器之一,您应该明确地将状态/历史数据(仅一次)添加到后端,通过调用$statemachine->add()(可选消息以指定创建机器的原因、位置或人员)来创建初始历史记录。

使用后端适配器的主要原因是可以永久存储转换记录的历史结构(用于分析和可能的会计)以及存储当前状态。在以后的时间重新创建状态机时,它将自动检索当前状态,您可以从上次停止的地方继续进行转换。

提供的所有持久化适配器都可以作为加载器使用,您可以从后端加载定义的状态机完整定义。每个后端可以配置多个状态机类型。有关使用加载器的详细信息,请参阅相应部分。

可以轻松编写针对不同后端的自定义适配器,以适应您特定的应用程序领域。您需要在后端创建适当的数据结构来满足您的需求,并通过子类化适配器编写一些自定义代码,以及读取和写入该后端的数据。提供的适配器可以指导您完成编写自己的适配器的过程。

持久化1. 在内存中存储状态数据

内存持久化适配器是默认适配器,如果您没有明确提供其他适配器,则由上下文使用。它仅在单个PHP进程中有用,因为状态仅在内存中持久化,因此当进程停止时状态会丢失。适用于使用场景包括PHP守护进程或具有有限生命周期的交互式PHP进程。请参阅examples/interactiveexamples/trafficlight,了解在内存中实现状态机的示例。

持久化2.在会话中存储状态数据

PHP会话可以用来存储数据。它们的寿命有限,但只要会话有效,它们就会在页面刷新之间持续存在。因此,它们适用于购物车、类似向导的表单以及其他具有页面刷新功能的HTML前端。请参阅examples/session,了解一个示例,该示例在页面刷新时切换彩虹颜色(作为状态机中的状态定义)。当PHP会话过期时,数据会丢失。

$adapter = new Session();
$machine = new StateMachine(new Context(new Identifier('session', 'rainbow-machine'), null, $adapter));
$machine->run();

session example

持久化3.在SQL后端中存储转换历史和状态数据

大多数应用程序中都有丰富的基于SQL的后端。PDO适配器提供了通过PDO驱动程序提供的所有后端的访问权限。在assets/sql中有完整的SQL模式,包括postgresql、mysql和sqlite,以及关于设计的完整文档(请参阅assets/sql/postgresql.sql)。一旦您创建了这些表,并提供了正确的凭证给PDO适配器,您就可以开始将状态存储在数据库中,您还可以完全定义您的机器,包括状态和转换及其相关的动作在表中。数据是永久存储的,为您提供所有机器的历史记录以及跟踪所有状态的方法,而无需在您的域对象表中存储状态。

$identifier('UUID-1234-ACD3-2156', 'data-migration-machine');
$adapter = new PDO('pgsql:host=localhost;port=5432;dbname=izzum');
//or for mysql
$adapter = new PDO('mysql:host=localhost;dbname=izzum');
//or for sqlite
$adapter = new PDO('"sqlite:izzum.db"');
$context = new Context($identifier, $builder, $adapter);
$statemachine = new StateMachine($context);
$adapter->load($statemachine);//the adapter can also act as a loader
$statemachine->add('creation of machine...');

持久化4.在Redis或MongoDB中存储转换历史和状态数据

Redis是一个NoSQL键/值数据库,MongoDB是一个基于文档的NoSQL数据库。两者都是无模式的,因此不需要配置即可开始存储状态和转换历史。Redis和MongoDB都提供了以JSON格式存储完整状态机配置的可能性(请参阅Loader示例以获取更多信息)。

$identifier('UUID-1234-ACD3-2156', 'data-migration-machine');
$adapter = new Redis('127.0.0.1', 6379);
//or use mongodb
$adapter = new MongoDB('mongodb://:27017');
$context = new Context($identifier, $builder, $adapter);
$statemachine = new StateMachine($context);
$adapter->load($statemachine);//the adapter can also act as a loader
$statemachine->add('creation of machine...');

加载状态机配置

通过使用提供的Loader类之一,您可以从JSONXMLYAML加载(多个)状态机定义。它们都可以从文件和字符串中加载数据。也可以使用提供的持久化适配器(redis和mongodb使用JSON格式,但可以被子类化以加载任何其他格式)来加载数据。

通过使用Loader类,您不必在PHP脚本中配置状态机,这使得维护和定义状态机更加容易和可重用。

Loader本身是一个接口,有一个简单的方法:Loader::load($statemachine):int,它填充状态机并返回添加的转换数。可以编写自定义加载器,最好将加载委托给LoaderArray类,该类已经为您(主要针对正则表达式状态和优先级)做了大量工作。如果想要扩展izzum实现,LoaderArray类可以与State、Transition和StateMachine的子类一起工作。

加载状态机配置:XML、JSON、YAML的示例

XML示例:请参阅assets/xml中的XML文件定义和与加载器一起使用的XML模式。加载器是izzum\loader\XML

$statemachine = new StateMachine(new Context(new Identifier('198442' , 't-shirt-production-facility-machine')));
$file = __DIR__ . '/machines.xml';
$loader = XML::createFromFile($file);
$loader->load($statemachine);
$statemachine->runToCompletion();

JSON示例:请参阅assets/json中的JSON文件定义和与加载器一起使用的JSON模式。加载器是izzum\loader\JSON

$statemachine = new StateMachine(new Context(new Identifier('btc-data-generator' , 'blockchain-parsing-machine')));
$file = __DIR__ . '/machines.json';
$loader = JSON::createFromFile($file);
$loader->load($statemachine);
$statemachine->runToCompletion();

YAML示例:请参阅assets/json中的YAML文件定义。加载器是izzum\loader\YAML

$statemachine = new StateMachine(new Context(new Identifier('wolverine' , 'mutant-machine')));
$file = __DIR__ . '/machines.yaml';
$loader = YAML::createFromFile($file);
$loader->load($statemachine);
$statemachine->runToCompletion();

加载状态机配置:SQL后端示例

请参阅assets\sql中的SQL模式(PostgreSQL、MySQL和SQLite),以及assets\sql\postresql.sql中提供的文档,了解如何将配置数据存储在模式中。只需在您选择的SQL后端中创建模式,将状态机配置填入表格中,然后在PHP中通过数据源名称(例如主机、端口、数据库名称)配置适配器,您就可以开始了。

$statemachine = new StateMachine(new Context(new Identifier('spiderman' , 'superhero-machine')));
$adapter = new PDO('pgsql:host=208.64.123.130;port=5432;dbname=izzum');
$adapter->load($statemachine);
$statemachine->run();

加载状态机配置:MongoDB和Redis示例

MongoDB和Redis持久化适配器也可以用作加载器。实现使用assets/json中指定的JSON。您应该在后端的一个特定位置加载JSON数据。对于MongoDB,您将JSON数据(在内部将转换为文档)存储在.configuration集合中。您可以在集合中存储多个配置,适配器将自动找到与集合中机器名称匹配的配置。对于Redis,如果您想在不同的键中使用多个配置,则将JSON字符串存储在<configurable-prefix>:configuration:<machine-name>键中。或者,如果您想在一个键中存储多个配置,则可以将JSON字符串存储在<configurable-prefix>:configuration键中。适配器将自动通过在特定键中匹配机器名称来找到配置,并回退到默认键。

对于这两个适配器,如果您将一个机器定义放在一个JSON字符串中,将更容易维护多个机器。请参阅tests\izzum\statemachine\persistence\RedisTesttests\izzum\statemachine\persistence\MongoDBTest以获取更多详细信息。

$redis = new Redis('127.0.0.1', 6379);
$machine = new StateMachine(new Context(new Identifier(1988442, 'crazy-machine'), null, $redis));
//set the configuration. Normally, this would be done directly on redis in a 
//seperate process before using the statemachine (eg: during deployment)
$configuration = file_get_contents(__DIR__ .'/redis-configuration-example.json');
$redis->set(Redis::KEY_CONFIGURATION, $configuration);
//load the machine
$redis->load($machine);

从不同的后端加载和存储数据:ReaderWriterDelegator

如果您想从特定来源加载数据,但想将数据写入不同的接收器,则应使用ReaderWriterDelegator类。它接受一个加载器和适配器实例。这样,您可以混合和匹配您想从哪里读取数据以及将其写入哪里。

$loader = XML::createFromFile(__DIR__ . '/configuration.xml');
$writer = new PDO('pgsql:host=208.64.123.130;port=5432;dbname=izzum');//postgres backend
$identifier = new Identifier('198442', 'awesome-machine');
$delegator = new ReaderWriterDelegator($loader, $writer);
$context = new Context($identifier, null, $delegator);
$machine = new StateMachine($context);
$delegator->load($machine);//loads from xml file
$machine->run();//stores data in postgres

###从状态机生成UML图来描述

###安装使用composer安装项目。创建一个名为composer.json的文件,包含以下行

{
    "require": {
        "rolfvreijdenberger/izzum-statemachine": "~4.0"
    }
}

然后使用以下命令安装包

composer install

您将在./vendor/rolfvreijdenberger/izzum-statemachine中找到izzum包。您也可以直接从GitHub下载它。该包应通过自动加载器(由composer默认提供)包含。

###运行单元测试您可以使用phpunit(通过composer安装)从命令行运行测试目录中的测试套件。

cd ./vendor/rolfvreijdenberger/izzum-statemachine/tests
phpunit -c phpunit.xml

默认情况下不会运行所有测试,因为持久化层测试依赖于不同的后端是否可用(PostgreSQL、MySQL、SQLite、MongoDB、Redis)以及/或PHP模块(yaml、redis、mongodb)。可以通过调整phpunit-xall.xml文件、安装正确的PHP模块和设置正确的后端来运行这些测试。

phpunit -c phpunit-all.xml