铁链/状态

0.5 2020-02-16 06:36 UTC

This package is auto-updated.

Last update: 2024-09-19 08:23:54 UTC


README

Build Status codecov Latest Stable Version License

IronBound State 是一个受 yohang/finite 影响很大的状态机库。

用法

use IronBound\State\Factory\StateMachineFactory;
use IronBound\State\Graph\GraphId;
use IronBound\State\Transition\TransitionId;

/** @var StateMachineFactory $stateMachineFactory */
$subject = new Order();
$machine = $stateMachineFactory->make($subject, new GraphId('payment'));
$state   = $machine->getCurrentState(); // State object for "unpaid"

$machine->apply(new TransitionId('pay'));
echo $subject->paymentStatus; // processing

foreach ($machine->getAvailableTransitions() as $transition) {
    echo $transition->getId();
}

if ($machine->evaluate(new TransitionId('refund'))->isValid()) {
    // do refund.
}

核心组件

主题

主题是维护状态并对其应用转换的对象。唯一的要求是它是一个 PHP 对象;没有为主题定义 interface

状态

State 对象代表主题当前所在的状态。例如,一篇文章可能处于“待定”状态或“已发布”状态。交货状态可能为“处理中”、“在承运人处等待”、“在途中”和“已交付”。

转换

Transition 对象定义了主题如何从一个状态移动到另一个状态。例如,当您“发布”一篇文章时,它将从“待定”状态移动到“已发布”状态。当您“交付给承运人”一个包裹时,它将从“处理中”状态转换到“在承运人处等待”。

Graph 对象负责持有主题可以处于的一组可用状态以及这些状态之间的转换列表。

主题可以拥有多个图。例如,一个电子商务订单可能有一个支付状态和一个交货状态。每个状态都是单独的图。

状态机

StateMachine 是与主题的状态交互以及在不同状态之间转换的方式。它可以告诉您从当前状态可用的转换,并应用转换以更改主题的状态。

状态调解器

主题可以以许多不同的方式存储和更改其状态。例如,它可以是实例属性、方法背后或可能完全独立于主题数据的地方跟踪。使用 StateMediator 可以将此细节从 StateMachine 中抽象出来。

工厂

使用 IronBound State 最直接的方法是直接实例化一个 ConcreteStateMachine

use IronBound\State\Graph\{GraphId, MutableGraph};
use IronBound\State\ConcreteStateMachine;
use IronBound\State\StateMediator\PropertyStateMediator;
use IronBound\State\State\{StateId, MutableState, StateType};
use IronBound\State\Transition\{TransitionId, MutableTransition};

$graph = new MutableGraph(new GraphId('status'));
$graph->addState(new MutableState(new StateId('pending'), StateType::INITIAL()));
$graph->addState(new MutableState(new StateId('published')));
$graph->addTransition(new MutableTransition(
    new TransitionId('publish'),
    [ new StateId('pending') ],
    new StateId('published')
));

$mediator = new PropertyStateMediator('status');

$stateMachine = new ConcreteStateMachine($mediator, $graph->toImmutable(), $subject);

但是,您可能更喜欢另一种构建风格,其中您使用预先配置的工厂。

use IronBound\State\Graph\GraphId;
use IronBound\State\Factory\StateMachineFactory;

/** @var StateMachineFactory $factory*/
$factory->make($subject, new GraphId('status'));

推荐的实现方法是使用 StateMachineFactoryConfigurator 类,根据配置数组执行繁重的工作。

use IronBound\State\Factory\StateMachineFactoryConfigurator;
use IronBound\State\Graph\GraphId;
use IronBound\State\State\StateType;
use IronBound\State\StateMachine;
use IronBound\State\Transition\Evaluation;

$config = [
    [
        // The test determines which Graphs apply to the given subject. This means that
        // GraphIds only have to be unique to the subject type instead of globally.
        'test'   => [
            // This particular test type checks if the subject is an instance of the given class. 
            'class' => 'Order',
        ],
        // The list of all the graphs for this subject.
        'graphs' => [
            'payment'  => [
                'mediator'    => [
                    // Use a mediator that checks against an object's properties.
                    'property' => 'paymentStatus',
                ],
                // The list of all the available states
                'states'      => [
                    'unpaid'   => [
                        // Specifies the StateType manually. The default is NORMAL.
                        'type' => StateType::INITIAL,
                    ],
                    // If you don't need any extra configuration options,
                    // you can just specify a string with no key.
                    'processing',
                    'paid',
                    'refunded' => [
                        'type' => StateType::FINAL,
                        // Custom defined attributes.
                        'attributes' => [
                            'label' => 'Refunded',
                        ],
                    ],
                ],
                // The list of all the available transitions
                'transitions' => [
                    'pay'      => [
                        // The list of states this transition can be applied from
                        'from' => 'unpaid',
                        // The state the subject will be in after transitioning. 
                        'to'   => 'processing',
                        // Custom defined attributes.
                        'attributes' => [
                            'label' => 'Pay',
                        ],
                    ],
                    'complete' => [
                        'from' => 'processing',
                        'to'   => 'paid',
                    ],
                    'refund'   => [
                        'from' => [ 'processing', 'paid' ],
                        'to'   => 'refunded',
                        // Use a guard to add constraints to when a transition is available. 
                        'guard' => static function(StateMachine $machine) {
                            if ($machine->getSubject()->createdAt + WEEK_IN_SECONDS < time()) {
                                return Evaluation::invalid('The refund window has expired.');
                            }
                            
                            return Evaluation::valid();
                        }
                    ],
                ],
            ],
            'delivery' => [
                'mediator'    => [
                    'property' => 'deliveryStatus',
                ],
                'states'      => [
                    'processing' => [
                        'type' => StateType::INITIAL,
                    ],
                    'in-transit',
                    'delivered'  => [
                        'type' => StateType::FINAL,
                    ]
                ],
                'transitions' => [
                    'drop-at-carrier' => [
                        'from' => 'processing',
                        'to'   => 'in-transit',
                    ],
                    'deliver'         => [
                        'from' => 'in-transit',
                        'to'   => 'delivered',
                    ],
                ],
            ],
        ],
    ],
    [
        'test'   => [
            'class' => 'BlogPost',
        ],
        'graphs' => [
            'states'      => [
                'draft'     => [
                    'type' => StateType::INITIAL,
                ],
                'published' => [
                    'type' => StateType::FINAL,
                ],
            ],
            'transitions' => [
                'publish' => [
                    'from' => 'draft',
                    'to'   => 'published',
                ],
            ],
        ],
    ],
];

$stateMachineFactory = (new StateMachineFactoryConfigurator())->configure( $config );

$paymentStateMachine = $stateMachineFactory->make(new Order(), new GraphId('payment'));
$deliveryStateMachine = $stateMachineFactory->make(new Order(), new GraphId('delivery'));
$blogPostStateMachine = $stateMachineFactory->make(new BlogPost(), new GraphId('status'));

事件

IronBound-State 与 PSR-14 Event Dispatcher 规范 集成,以自定义行为并监听动作。

您可以通过调用 ConcreteStateMachine::setEventDispatcherConcreteStateMachine 提供一个 EventDispatcherInterface 实例。目前支持以下事件。

TestTransitionEvent

在评估过程之后被调用,确定转换可用且守卫返回有效评估。通过调用 TestTransitionEvent::reject($reason) 动态阻止应用转换。

BeforeTransitionEvent

在应用转换后更新主题状态之前被调用。

AfterTransitionEvent

在应用转换后更新主题状态之后被调用。