sofa/state-machine

易于使用的有限状态机实现

v0.3 2020-11-29 05:19 UTC

This package is auto-updated.

Last update: 2024-08-29 04:54:27 UTC


README

有限状态机 实现

Downloads stable Coverage Status

安装

将包添加到您的项目中

path/to/your/app$ composer require sofa/state-machine

用法

状态机帮助您消除代码中的 switch 和/或 if/else 语句,以确定给定状态下的可用操作。

让我们用一个来自 Laravel Blade 视图模板和底层 Eloquent Order 模型的简单示例来说明

@foreach($orders as $order)
    {{ $order->reference }} status: {{ $order->status }}

    @if($order->status === 'new')
        <button>start processing</button>

    @elseif($order->status === 'awaiting_payment')
        <button>record payment</button>

    @elseif($order->status === 'awaiting_shipment')
        <button>save tracking number</button>

    @elseif($order->status === 'in_delivery')
        <button>record delivery</button>
        <button>open claim</button>

    @elseif($order->status === 'complete')
        <button>open claim</button>

    @elseif($order->status === 'processing_claim')
        <button>refund</button>
        <button>close claim</button>
    @endif
@endforeach

这很快就会变得难以控制,尤其是在引入新的状态或处理顺序发生变化时。

为了使其流程化,我们可以为订单实体实现状态机

  1. Order 模型上实现接口

    class Order extends Model implements \Sofa\StateMachine\StateMachineInterface
    {
        //...
    
        public function getCurrentState() : string
        {
            return $this->status;
        }
    
        public function setState(string $state) : void
        {
            $this->status = $state;
            $this->save();
        }
    }
  2. 定义可用转换并准备模板数据

    $transitions = [
        Transition::make(/*from_state*/ 'new', /*action*/ 'start processing', /*to_state*/ 'awaiting_payment'),
        Transition::make('awaiting_payment', 'record payment', 'awaiting_shipment'),
        Transition::make('awaiting_shipment', 'save tracking number', 'in_delivery'),
        Transition::make('in_delivery', 'record delivery', 'complete'),
        Transition::make('in_delivery', 'open claim', 'processing_claim'),
        Transition::make('complete', 'open claim', 'processing_claim'),
        Transition::make('processing_claim', 'close claim', 'complete'),
        Transition::make('processing_claim', 'refund', 'refunded'),
    ];
    
    foreach ($orders as $order) {
        $order_state = new \Sofa\StateMachine\Fsm($order, $transitions);
    
        $order->available_actions = $order_state->getAvailableActions();
    }
  3. 这样我们就得到了控制器和模板代码与处理逻辑及订单的解耦

    @foreach($orders as $order)
        {{ $order->reference }} status: {{ $order->status }}
    
        @foreach($order->available_actions as $action)
            <button>{{ $action }}</button>
        @endforeach
    @endforeach
  4. 最后让我们处理操作

    // controller handling the action
    public function handleAction($order_id, Request $request)
    {
        $order_state = new \Sofa\StateMachine\Fsm(Order::find($order_id), $transitions);
    
        $this->validate($request, [
            'action' => Rule::in($order_state->getAvailableActions()),
            // ...
        ]);
        $order_state->process($request->get('action'));
    
        return Redirect::to('some/place');
    }

有了这个设置,我们就不再需要在业务需求改变时更改我们的控制器或视图,而是向状态机定义中添加一个新的转换。

我需要在转换过程中有更多的控制 - 如何做?

上面的示例假设了一个非常简单的转换过程,即 $order->status = $new_status。这有时可能足够,但通常我们需要在转换过程中有更多的灵活性。为了满足这一需求,您可以自定义您的 Transition 定义,使它们从简单的 POPO 转变为在状态机处理适当的 action 时将被调用的 callable

class Refund extends \Sofa\StateMachine\Transition
{
    public function __invoke(StateMachineInterface $order, $payload)
    {
        // $payload is any object you pass to the process method:
        // $order_state->process('refund', $anything_you_need_here);
        $order->refunded_at = $payload['time'];
        $order->refunded_by = $payload['user_id'];

        $order->setState($this->to_state);
    }
}

// Then our transitions definition would like something like:
$transitions = [
    // ...
    Transition::make('processing_claim', 'close claim', 'complete'),
    Refund::make('processing_claim', 'refund', 'refunded'),
];

编码愉快!

贡献

欢迎所有贡献。确保您的 PR 符合 PSR-2 规范并经过测试。