soap/eloquent-workflow

Laravel Eloquent 简单状态和转换工作流

v0.1.0-beta.2 2023-01-29 15:37 UTC

README

Latest Version on Packagist GitHub Tests Action Status GitHub Code Style Action Status Total Downloads

此包为 eloquent 模型添加状态转换工作流。用户可以选择从可用的转换中进行操作,使 eloquent 模型从一个状态转换到另一个状态。它基于 codewiser/workflow 的大量工作 codewiser/workflow

支持我们

安装

您可以通过 composer 安装此包

composer require soap/eloquent-workflow

您可以使用以下命令发布并运行迁移

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

您可以使用以下命令发布配置文件

php artisan vendor:publish --tag="eloquent-workflow-config"

这是已发布配置文件的内容

return [
];

可选地,您可以使用以下命令发布视图

php artisan vendor:publish --tag="eloquent-workflow-views"

用法

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

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

设置

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

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

您可以使用 Enum 代替标量值

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

use \Soap\EloquentWorkflow\Example\Enum;
use \Soap\EloquentWorkflow\Example\ArticleWorkflow;
use \Soap\EloquentWorkflow\StateMachineEngine;

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

就这样。

一致性

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

use \Soap\EloquentWorkflow\Example\Enum;

// creating: will set proper initial state
$article = new \Soap\EloquentWorkflow\Example\Article();
$article->save();
assert($article->state == 'new');

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

状态和转换对象

在上面的示例中,我们使用标量值描述了蓝图,但实际上它们将被转换为对象。这些对象为状态和转换带来了额外的功能,例如标题翻译、转换授权、路由规则等...

use \Soap\EloquentWorkflow\State;
use \Soap\EloquentWorkflow\Transition;

class ArticleWorkflow extends \Soap\EloquentWorkflow\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 定义转换授权规则。

使用策略

提供能力名称

use \Soap\EloquentWorkflow\Transition;

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

使用闭包

use \Soap\EloquentWorkflow\Transition;
use \Illuminate\Support\Facades\Gate;

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

授权转换

要获取当前用户授权的唯一转换,请使用 TransitionCollectionauthorized 方法

$article = new \Soap\EloquentWorkflow\Example\Article();

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

授权转换

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

public function update(Request $request, \Soap\EloquentWorkflow\Example\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();
}

业务逻辑

禁用转换

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

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

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

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

use \Soap\EloquentWorkflow\Transition;
use \Soap\EloquentWorkflow\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 \Soap\EloquentWorkflow\Transition;
use \Soap\EloquentWorkflow\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 \Soap\EloquentWorkflow\Transition;

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

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

use Illuminate\Http\Request;

public function update(Request $request, \Soap\EloquentWorkflow\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 对象。

use \Soap\EloquentWorkflow\State;
use \Soap\EloquentWorkflow\Transition;
use \Soap\EloquentWorkflow\WorkflowBlueprint;

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

附加属性

有时我们需要向工作流状态和转换添加一些附加属性。例如,我们可以按级别分组状态,并使用这些信息在用户界面中为状态和转换着色。

use \Soap\EloquentWorkflow\State;
use \Soap\EloquentWorkflow\Transition;
use \Soap\EloquentWorkflow\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(\App\Models\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"
    }
  ]
}

事件

状态回调

您可以为达到状态时定义状态回调。

回调是一个具有 Modelcontext 参数的 callable

use \Soap\EloquentWorkflow\State;

State::make('correcting')
    ->after(function(Article $article, ?State $previous, array $context) {
        $article->author->notify(
            new ArticleHasProblemNotification(
                $article, $context['reason']
            )
        );
    }); 

转换回调

您可以为成功执行转换后定义转换回调。

回调是一个具有 Modelcontext 参数的 callable

use \Soap\EloquentWorkflow\State;
use \Soap\EloquentWorkflow\Transition;

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

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

事件监听器

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

use \Soap\EloquentWorkflow\Events\ModelTransited;

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

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

转换日志

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

config/app.phpproviders 部分中注册 \Soap\EloquentWorkflow\EloquentWorkflowServiceProvider

运行迁移

php artisan migrate

workflow.logging 编辑到 config/eloquent-workflow.php

    'workflow' => [
        'logging' => true
    ]

完成了。

要获取日志记录,请将 \Soap\EloquentWorkflow\Traits\HasTransitionLog 添加到具有工作流的 Model。它带来了 transitions 关系。

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

蓝图验证

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

config/app.phpproviders 部分中注册 \Soap\EloquentWorkflow\WorkflowServiceProvider

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

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

测试

composer test

变更日志

有关最近更改的更多信息,请参阅 变更日志

贡献

有关详细信息,请参阅 贡献指南

安全漏洞

有关如何报告安全漏洞的详细信息,请参阅 我们的安全策略

鸣谢

许可证

MIT 许可证(MIT)。有关更多信息,请参阅 许可证文件