vegeta/fluxer

简单灵活的多用途状态机和工作流库

dev-master 2014-11-12 18:17 UTC

This package is not auto-updated.

Last update: 2024-09-25 22:42:28 UTC


README

这是一个简单而灵活的PHP有限状态机。这个库深受C# Stateless (https://github.com/nblumhardt/stateless) 项目的启发。可以直接在PHP代码中创建状态机和工作流。

require 'vendor/autoload.php';
use Fluxer\StateMachine;

$phone = new StateMachine('offHook');
$phone->forState('offHook')
	->permit('callDialed', 'ringing');

$phone->forState('ringing')
	->permit('hungUp', 'offHook')
	->permit('callConnected', 'connected');

$phone->forState('connected')
	->onEntry(function($event) { startTimer();	})
	->onExit(function($event) { stopTimer(); })
	->permit('leftMessage', 'offHook')
	->permit('hungUp', 'offHook')
	->permit('placedOnHold', 'onHold');

$phone->forState('onHold')
	->permit('takenOffHold', 'connected')
	->permit('hungUp', 'offHook');

// execute workflow
$phone->fire('callDialed');
echo $phone->getState(); // ringing

要求

此库需要PHP 5.3.2+。库的根命名空间为Fluxer

安装(通过composer)

{
    "require": {
        "vegeta/fluxer": "dev-master"
    }
}

包括使用Composer自动加载器。

require 'vendor/autoload.php';
use Fluxer\StateMachine;

$stateMachine = new StateMachine();

特性

  • 流畅的API进行配置
  • 对分层状态的初始支持
  • 支持使用字符串或标量常量作为状态和触发器
  • 状态的状态进入/退出事件
  • 使用变量或闭包的(守卫子句)条件转换
  • 任何给定状态支持的转换列表(触发器 -> 目标)

基本用法

配置

创建一个类型为StateMachine的对象,使用forState($originState)函数配置状态,并使用permit($trigger, $destinationState)函数配置转换。转换是在给定某些动作的情况下状态的变化,在这种情况下称为“触发器”。它表示为:“如果状态是$originState,并且调用$trigger动作,则移动到$destinationState”。

$machine = new StateMachine();
$machine->forState('checkout')
	->permit('create', 'pending')
	->permit('confirm', 'confirmed');

$machine->forState('pending')
	->permit('confirm', 'confirmed');

$machine->forState('confirmed')
	->permit('cancel', 'cancelled');

当没有为它声明转换时,状态是“最终”状态,在上面的示例中,“已取消”是最终状态。如果您想在该状态达到时触发事件,可以为此最终状态添加配置。

注意:如果使用常量作为状态或触发器,请确保它们具有不解析为PHP的空(0、false、null、'')的值。坚持使用字符串(文字或常量)通常是最好的选择。

运行状态机

一旦配置了状态和转换,设置初始状态并触发以更改状态。

$machine->init('checkout');
$machine->fire('create');
$machine->fire('confirm');
$machine->fire('cancel');

echo $machine->getState(); // cancelled

初始状态可以通过StateMachine构造函数设置,也可以在任何时候显式调用init($initialState)函数来设置,例如,在从外部数据源获取状态时。

您可以在任何时候检查是否可以触发触发器以及当前状态的完整可用触发器列表(自省)。

$machine->canFire('checkout'); // true or false
$allowed = $machine->allowedTriggers(); // associative array: trigger => destination state

无效触发器控制

如果使用fire()方法调用无效触发器,默认情况下将引发StateMachineException类型的异常。可以通过接受一个回调的onUnhandledTrigger()方法更改此行为,该回调将在无效触发器被触发时执行。此函数只有一个参数,即一个数组,包含当前的“状态”和尝试的“触发器”。

事件和回调

状态机允许在达到状态之前执行回调函数(onEntry)、在每次成功转换时执行(onTransition)以及离开状态后执行(onExit)。

事件定义为以下签名的回调函数:function ($transition, $userData) {},其中$transition是包含当前转换信息('source'、'trigger'、'destination')的数组,$userData是传递给事件的任何用户定义值,可以是当前数据上下文或用于fire()、canFire()或allowedTriggers()调用的自定义参数。

回调可以是任何合格为“可调用的”东西,即匿名函数或对php中常见的数组(object|class, method)形式的类或对象方法的引用。

class Logger {
	static function logTransition($transition, $data) {
		// do stuff
		echo 'with ' . $transition['trigger'] . ' : ' . $transition['source'] 
			. ' -> ' . $transition['destination'];
		echo "\n";
	}
}
...

$machine->onTransition(array('Logger', 'logTransition')) // using call to static function
	->forState('checkout')
	->permit('create', 'pending')
	->permit('confirm', 'confirmed');

$machine->forState('confirmed')
   	->onEntry(function($transition, $data) { // anonymous function
   		// send email to customer
   		sendEmail($data->customer, 'Order confirmed');
   	})
   	->permit('cancel', 'cancelled');

// state events defined at state machine level
$machine->onExitFor('checkout', function($transition, $data) {
		// perform action after checkout	
	})->onExitFor('checkout', function($transition, $data) {
		// perform a secondary action after checkout
	});

...

如有需要,您可以为同一事件添加多个回调函数,并且它们将按照定义的顺序执行。onEntry() 和 onExit() 函数在状态配置级别(forState())可用,但您还可以使用 onEntryFor($state, $callable) 和 onExitFor($state, $callable) 在状态机级别定义事件,以便更清晰地组织代码。

此外,您可以使用 setStateMutator($callable) 方法定义一个在每次状态改变时被调用的函数。此函数只接受新状态作为单个参数。

分层状态

以下示例中,状态 'onHold' 是 'connected' 的子状态,这意味着除了 'onHold' 之外的 'connected' 的所有触发器都将可用。要检查当前状态是否属于父状态,请使用 isInState() 函数,该函数将检查当前状态以及(如果有)其父状态。

$fluxer = new StateMachine();
$fluxer->forState('offHook')
	->permit('callDialed', 'ringing');

$fluxer->forState('ringing')
	->permit('hungUp', 'offHook')
	->permit('callConnected', 'connected');

$fluxer->forState('connected')
	->permit('leftMessage', 'offHook')
	->permit('hungUp', 'offHook')
	->permit('placedOnHold', 'onHold');

$fluxer->forState('onHold')
	->substateOf('connected')
	->permit('takenOffHold', 'connected')
	->permit('hungUp', 'offHook')
	->permit('hurlPhoneToWall', 'DESTROYED');

$fluxer->fire('callDialed');
$fluxer->fire('callConnected');
$fluxer->fire('placedOnHold');
assert( $fluxer->isInState('connected') ); // onHold is a substate of connected
$allowed = $fluxer->allowedTriggers();
assert( isset($allowed['leftMessage']) ); // trigger for parent state is available in substate as well

在状态机中使用外部数据

您可以使用数据上下文或将参数直接传递给执行函数(如 fire())的方式,在状态机中使用外部数据。

数据上下文可以是一个数组、对象或一个返回数据的函数。

$machine = new StateMachine();
// using an anonymous function
$machine->withDataContext(function() {
	return array('number'=>5, 'string'=>'hello');
});

// using an object
$user = new User();
$user->username = 'joe';
$user->role = 'salesman';

$machine->withDataContext($user);

// passing parameter directly to call
$machine->fire('checkout', $user);
$machine->canFire('cancel', $user);

在直接将参数传递给 fire()、canFire() 或 allowedTriggers() 时,此值将仅覆盖该特定调用当前数据上下文。建议尽可能使用数据上下文。

条件转换

可以通过为 permit() 函数的第三个参数定义一个变量或返回 true 或 false 的可调用函数来配置转换,使其仅在满足条件时触发。

以下示例(简单的汽车工作流程)中,状态 'engineStarted' 的触发器 'drive' 依赖于一个条件,该条件读取数据上下文中 parkingBrake 变量的值,在这种情况下,是一个简单的对象。

// create an object to act as external data for the state machine
$car = new stdClass();
$car->parkingBrake = 'on';

$flow = new StateMachine();
$flow->withDataContext($car)
	->forState('engineStarted')
	->permit('drive', 'running', function ($event, $data) {
		// allow to drive only if the parking brake is off
		return $data->parkingBrake == 'off';
	})
	->permit('stopEngine', 'stopped');

$flow->init('engineStarted');
$car->parkingBrake = 'on';
assert( $flow->canFire('drive') == false );
$car->parkingBrake = 'off';
assert( $flow->canFire('drive') == true );
$flow->fire('drive');
assert( $flow->getState() == 'running' );

动态状态解析

有时,转换的 目标状态需要通过查询或计算在运行时确定。如果是这种情况,可以使用 permitDynamic() 调用配置转换,以函数返回实际目标状态。该函数具有与标准事件相同的签名,并且必须返回一个有效状态。

以下示例中,在提交表单后,必须对表单进行多次审查,然后才能进一步处理。最大次数是通过外部确定的。

...

$flow->forState('submitted')
	->permit('review', 'reviewed');

$flow->forState('reviewed')
	->permitDynamic('review', function($transition, $data) {
		if($data['reviewCount'] < $data['maxReviewers']) {
			return 'reviewed'; // still need more reviewing 
		}
		// the maximum number of reviews is complete, move on to the next state
		return 'reviewComplete';
	});
	
...

// these values could come from a database
$data['maxReviewers'] = 3;
$data['reviewCount'] = 2;

$flow->init('submitted');
$flow->fire('review', $data);
assert( $flow->getState() == 'reviewed' );
$data['reviewCount']++;
$flow->fire('review', $data);
assert( $flow->getState() == 'reviewComplete' );

当使用 introspection 与 allowedTriggers() 获取可能的转换列表时,动态转换的目标状态将显示为 '?',除非将 'evalDynamic' 参数设置为 true,在这种情况下,将使用数据上下文或提供的参数评估所有动态转换。

如果函数没有返回一个有效状态,则会引发 StateMachineException 异常。

示例

示例状态机和工作流程可以在 /examples 文件夹中找到。

TODO

  • 更好地控制重入转换

##许可证##

版权所有 (c) 2014 Manolo Gomez

在 Apache 2.0 许可证下发布。