codewiser/workflow

模型工作流Laravel包

v4.5.3 2024-09-11 09:41 UTC

README

包为Eloquent模型提供工作流功能。

工作流是一系列状态,文档通过这些状态演变。状态之间的转换影响演变路径。

设置

首先,用可用的状态和转换描述工作流程蓝图

class ArticleWorkflow extends \Codewiser\Workflow\WorkflowBlueprint
{
    public function states(): array
    {
        return [
            'new',
            'review',
            'published',
            'correction',
        ];
    }
    
    public function transitions(): array
    {
        return [
            ['new', 'review'],
            ['review', 'published'],
            ['review', 'correction'],
            ['correction', 'review']
        ];
    }
}

您可以使用Enum而不是标量值。

class ArticleWorkflow extends \Codewiser\Workflow\WorkflowBlueprint
{
    public function states(): array
    {
        return Enum::cases();
    }
    
    public function transitions(): array
    {
        return [
            [Enum::new, Enum::review],
            [Enum::review, Enum::published],
            [Enum::review, Enum::correction],
            [Enum::correction, Enum::review]
        ];
    }
}

接下来,包括特性和创建方法以将蓝图绑定到模型的属性。

use \Codewiser\Workflow\Example\Enum;
use \Codewiser\Workflow\Example\ArticleWorkflow;
use \Codewiser\Workflow\StateMachineEngine;

class Article extends Model
{
    use \Codewiser\Workflow\Traits\HasWorkflow;
    
    public function state(): StateMachineEngine
    {
        return $this->workflow(ArticleWorkflow::class, 'state');
    }
}

就是这样。

一致性

工作流观察模型并保持状态机的一致性。

use \Codewiser\Workflow\Example\Enum;

// creating: will set proper initial state
$article = new \Codewiser\Workflow\Example\Article();
$article->save();
assert($article->state == Enum::new);

// updating: will examine state machine consistency
$article->state = Enum::review;
$article->save();
// No exceptions thrown
assert($article->state == Enum::review);

状态和转换对象

在上面的例子中,我们用标量值描述了蓝图,但实际上它们将被转换成对象。这些对象为状态和转换提供了额外的功能,例如标题翻译、过渡授权、路由规则、前转换和后转换回调等...

use \Codewiser\Workflow\State;
use \Codewiser\Workflow\Transition;

class ArticleWorkflow extends \Codewiser\Workflow\WorkflowBlueprint
{
    public function states(): array
    {
        return [
            State::make('new'),
            State::make('review'),
            State::make('published'),
            State::make('correction'),
        ];
    }
    
    public function transitions(): array
    {
        return [
            Transition::make('new', 'review'),
            Transition::make('review', 'published'),
            Transition::make('review', 'correction'),
            Transition::make('correction', 'review'),
        ];
    }
}

授权

由于模型的操作不允许任何用户执行,因为不允许任何用户更改状态。您可以使用Policy或使用callable来定义转换授权规则。

使用Policy

提供能力名称。包将检查给定的能力是否与相关模型关联。

use \Codewiser\Workflow\Transition;

Transition::make('new', 'review')
    ->authorizedBy('transit');

class ArticlePolicy
{
    public function transit(User $user, Article $article, Transition $transition)
    {
        //
    }
}

使用闭包

授权闭包可以返回truefalse,或者抛出AuthorizationException

use \Codewiser\Workflow\Transition;
use \Illuminate\Support\Facades\Gate;

Transition::make('new', 'review')
    ->authorizedBy(fn(Article $article, Transition $transition) => Gate::authorize('transit', [$article, $transition]));

授权转换

要获取当前用户有权限的转换,请使用TransitionCollectionauthorized方法

$article = new \Codewiser\Workflow\Example\Article();

$transitions = $article->state()
    // Get transitions from model's current state.
    ->transitions()
    // Filter only authorized transitions. 
    ->onlyAuthorized();

授权转换

在处理用户请求时,不要忘记授权工作流状态更改。

public function update(Request $request, Article $article)
{
    $this->authorize('update', $article);
    
    if ($state = $request->input('state')) {
        // Check if user allowed to make this transition
        $article->state()->authorize($state);
    }
    
    $article->fill($request->validated());
    
    $article->save();
}

可收费转换

只有当累积了一些费用时,可收费的转换才会触发。例如,我们可能只想在至少三位编辑接受后发布文章。

use \Codewiser\Workflow\Charge;
use \Codewiser\Workflow\Transition;

Transition::make('review', 'publish')
    ->chargeable(Charge::make(
        progress: function(Article $article) {
            return $article->accepts / 3;
        },
        callback: function (Article $article) {
            $article->accepts++;
            $article->save();
        }
    ));

Charge类有更多选项,可以提供投票统计或防止重复投票。

业务逻辑

禁用转换

转换可能对模型有一些先决条件。如果模型符合这些条件,则转换是可能的。

先决条件是一个具有Model参数的callable。它可以抛出异常。

要临时禁用转换,先决条件应该抛出TransitionRecoverableException。在异常消息中留下帮助说明。

以下是一些用户可能解决的问题的示例。

use \Codewiser\Workflow\Transition;
use \Codewiser\Workflow\Exceptions\TransitionRecoverableException;

Transition::make('new', 'review')
    ->before(function(Article $model) {
        if (strlen($model->body) < 1000) {
            throw new TransitionRecoverableException(
                'Your article should contain at least 1000 symbols. Then you may send it to review.'
            );
        }
    })
    ->before(function(Article $model) {
        if ($model->images->count() == 0) {
            throw new TransitionRecoverableException(
                'Your article should contain at least 1 image. Then you may send it to review.'
            );
        }
    });

用户将在可用转换的列表中看到有问题的转换。用户遵循说明解决问题后,可以再次尝试执行转换。

移除转换

在某些情况下,工作流程路由可能分成分支。由业务逻辑强制执行的方式,而不是用户。用户甚至不应该知道其他方式。

要完全从列表中删除转换,先决条件应该抛出TransitionFatalException

use \Codewiser\Workflow\Transition;
use \Codewiser\Workflow\Exceptions\TransitionFatalException;

Transition::make('new', 'to-local-manager')
    ->before(function(Order $model) {
        if ($model->amount >= 1000000) {
            throw new TransitionFatalException("Order amount is too big for this transition.");
        }
    }); 

Transition::make('new', 'to-region-manager')
    ->before(function(Order $model) {
        if ($model->amount < 1000000) {
            throw new TransitionFatalException("Order amount is too small for this transition.");
        }
    }); 

用户将根据订单数量值看到唯一的转换。

附加上下文

有时应用程序需要额外的上下文来执行转换。例如,这可能是因为审稿人拒绝文章的原因。

首先,在转换或状态定义中声明验证规则

use \Codewiser\Workflow\Transition;

Transition::make('review', 'reject')
    ->rules([
        'reason' => 'required|string|min:100'
    ]);

接下来,在控制器中设置上下文。

当创建模型时

use Illuminate\Http\Request;

public function store(Request $request)
{
    $this->authorize('create', \Codewiser\Workflow\Example\Article::class);
    
    $article = \Codewiser\Workflow\Example\Article::query()->make(
        $request->all()
    );
    
    $article->state()
        // Init workflow, passing additional context
        ->init($request->all())
        // Now save model
        ->save();
}

当转换模型时

use Illuminate\Http\Request;

public function update(Request $request, \Codewiser\Workflow\Example\Article $article)
{
    $this->authorize('update', $article);
    
    if ($state = $request->input('state')) {
        $article->state()
            // Authorize transition
            ->authorize($state)
            // Transit to the new state, passing additional context
            ->transit($state, $request->all())
            // Now save model
            ->save();        
    }
}

上下文将在保存时进行验证,您可以捕获ValidationException

最后,您可以在事件中处理此上下文。

翻译

您可以使用具有可翻译标题的 StateTransition 对象。然后,使用枚举实现 \Codewiser\Workflow\Contracts\StateEnumenum

没有标题的 Transition 将继承其目标 State 的标题。

use \Codewiser\Workflow\State;
use \Codewiser\Workflow\Transition;
use \Codewiser\Workflow\WorkflowBlueprint;

class ArticleWorkflow extends WorkflowBlueprint
{
    protected function states(): array
    {
        return [
            State::make('new')->as(__('Draft')),
            State::make('published')->as(fn(Article $model) => __('Published'))
        ];
    }
    protected function transitions(): array
    {
        return [
            Transition::make('new', 'published')->as(__('Publish'))
        ];
    }
}

附加属性

有时我们需要向工作流程状态和转换添加一些附加属性。例如,我们可以按级别对状态进行分组,并使用此信息在用户界面中着色状态和转换。然后,使用枚举实现 \Codewiser\Workflow\Contracts\StateEnumenum

Transition 继承其目标 State 的属性。

use \Codewiser\Workflow\State;
use \Codewiser\Workflow\Transition;
use \Codewiser\Workflow\WorkflowBlueprint;

class ArticleWorkflow extends WorkflowBlueprint
{
    protected function states(): array
    {
        return [
            State::make('new'),
            State::make('review')     ->set('level', 'warning'),
            State::make('published')  ->set('level', 'success'),
            State::make('correction') ->set('level', 'danger')
        ];
    }
    protected function transitions(): array
    {
        return [
            Transition::make('new', 'review')         ->set('level', 'warning'),
            Transition::make('review', 'published')   ->set('level', 'success'),
            Transition::make('review', 'correction')  ->set('level', 'danger'),
            Transition::make('correction', 'review')  ->set('level', 'warning')
        ];
    }
}

JSON 序列化

为了让用户与模型的流程交互,我们应该将数据传递给应用程序的前端。

use Illuminate\Http\Request;

public function state(\Codewiser\Workflow\Example\Article $article)
{    
    return $article->state()->toArray();
}

负载将如下所示

{
  "value": "review",
  "name": "Review",
  "transitions": [
    {
      "source": "review",
      "target": "publish",
      "name": "Publish",
      "issues": [
        "Publisher should provide a foreword."
      ],
      "level": "success"
    },
    {
      "source": "review",
      "target": "correction",
      "name": "Send to Correction",
      "rules": {
        "reason": ["required", "string", "min:100"]
      },
      "level": "danger"
    }
  ]
}

事件

状态回调

您可以定义状态回调,当达到状态时将调用这些回调。

回调是一个带有 Model 和可选 Transition 参数的 callable

use \Codewiser\Workflow\Context;
use \Codewiser\Workflow\State;
use \Codewiser\Workflow\Transition;

State::make('correcting')
    ->rules(['reason' => 'required|string|min:100'])
    ->after(function(Article $article, Context $context) {
        $article->author->notify(
            new ArticleHasProblemNotification(
                $article, $context->data()->get('reason')
            )
        );
    }); 

转换回调

您可以定义转换回调,在转换成功执行后调用。

这与状态回调完全相同。

use \Codewiser\Workflow\Context;
use \Codewiser\Workflow\Transition;

Transition::make('review', 'correcting')
    ->rules(['reason' => 'required|string|min:100'])
    ->after(function(Article $article, Context $context) {
        $article->author->notify(
            new ArticleHasProblemNotification(
                $article, $context->data()->get('reason')
            )
        );
    }); 

您可以为单个转换定义多个回调。

事件监听器

转换生成 ModelTransited 事件。您可以为它定义 EventListener

use \Codewiser\Workflow\Events\ModelTransited;

class ModelTransitedListener
{
    public function handle(ModelTransited $event)
    {
        if ($event->model instanceof Article) {
            $article = $event->model;

            if ($event->context->target()->is('correction')) {
                // Article was send to correction, the reason described in context
                $article->author->notify(
                    new ArticleHasProblemNotification(
                        $article, $event->context->data()->get('reason')
                    )
                );
            }
        }
    }
}

转换历史记录

该包可以将转换记录到数据库表中。

config/app.phpproviders 部分中注册 \Codewiser\Workflow\WorkflowServiceProvider

workflow.history 添加到 config/services.php

    'workflow' => [
        'history' => true
    ]

发布并运行迁移

php artisan vendor:publish --tag=workflow-migrations
php artisan migrate

完成了。

要获取历史记录,将 \Codewiser\Workflow\Traits\HasTransitionHistory 添加到具有工作流程的 Model 中。它带来了 transitions 关系。

历史记录由 \Codewiser\Workflow\Models\TransitionHistory 模型表示,该模型包含有关转换执行者、源和目标状态以及上下文(如果提供)的信息。

蓝图验证

该包可以验证您定义的工作流程蓝图。

config/app.phpproviders 部分中注册 \Codewiser\Workflow\WorkflowServiceProvider

使用蓝图类名运行控制台命令

php artisan workflow:blueprint --class=App/Workflow/ArticleWorkflow