wlhrtr/state-machine

Eloquent 模型状态机的实现(基于 asantibanez 的代码进行扩展)

1.0.0 2024-06-27 12:12 UTC

This package is not auto-updated.

Last update: 2024-09-17 12:57:19 UTC


README

Latest Version on Packagist Total Downloads

Laravel Eloquent State Machines

介绍

此包允许您通过在特定的 StateMachine 类中定义转换逻辑来简化 Eloquent 模型状态的转换。每个类都允许您注册验证、钩子和允许的转换和状态,使得每个 StateMachine 类成为从一个状态转换到下一个状态时唯一的真理来源。

Laravel Eloquent 状态机还允许您自动记录模型可能具有的所有状态的历史,并根据具体情况查询此历史。

此包的核心是创建一个简单但功能强大的 API,以便 Laravel 开发者能够感到宾至如归。

示例

具有两个状态字段的模型

$salesOrder->status; // 'pending', 'approved', 'declined' or 'processed'

$salesOrder->fulfillment; // null, 'pending', 'completed'

从一个状态转换到另一个状态

$salesOrder->status()->transitionTo('approved');

$salesOrder->fulfillment()->transitionTo('completed');

//With custom properties
$salesOrder->status()->transitionTo('approved', [
    'comments' => 'Customer has available credit',
]);

//With responsible
$salesOrder->status()->transitionTo('approved', [], $responsible); // auth()->user() by default

检查可用的转换

$salesOrder->status()->canBe('approved');

$salesOrder->status()->canBe('declined');

检查当前状态

$salesOrder->status()->is('approved');

$salesOrder->status()->responsible(); // User|null

检查转换历史

$salesOrder->status()->was('approved');

$salesOrder->status()->timesWas('approved');

$salesOrder->status()->whenWas('approved');

$salesOrder->fulfillment()->snapshowWhen('completed');

$salesOrder->status()->history()->get();

演示

您可以在此查看演示和示例

demo

安装

您可以通过 composer 安装此包

composer require wlhrtr/state-machine

接下来,您必须导出包迁移

php artisan vendor:publish --provider="Wlhrtr\StateMachine\StateMachineServiceProvider" --tag="migrations"

最后,准备所需的数据库表

php artisan migrate

使用方法

定义我们的 StateMachine

假设我们有一个 SalesOrder 模型,它有一个 status 字段来跟踪我们的销售订单在系统中的不同阶段:REGISTEREDAPPROVEDPROCESSEDDECLINED

我们可以在 StateMachine 类中管理和集中所有这些阶段和转换。要定义一个,我们可以使用 php artisan make:state-machine 命令。

例如,我们可以为我们的 SalesOrder 模型创建一个 StatusStateMachine

php artisan make:state-machine StatusStateMachine

运行命令后,我们将在 App\StateMachines 目录中创建一个新的 StateMachine 类。该类将包含以下代码。

use Wlhrtr\StateMachine\StateMachines\StateMachine;

class StatusStateMachine extends StateMachine
{
    public function recordHistory(): bool
    {
        return false;
    }

    public function transitions(): array
    {
        return [
            //
        ];
    }

    public function defaultState(): ?string
    {
        return null;
    }
}

在这个类中,我们可以定义我们的状态和允许的转换

public function transitions(): array
{
    return [
        'pending' => ['approved', 'declined'],
        'approved' => ['processed'],
    ];
}

允许使用通配符来允许任何状态改变

public function transitions(): array
{
    return [
        '*' => ['approved', 'declined'], // From any to 'approved' or 'declined'
        'approved' => '*', // From 'approved' to any
        '*' => '*', // From any to any
    ];
}

我们还可以定义默认/起始状态

public function defaultState(): ?string
{
    return 'pending'; // it can be null too
}

StateMachine 类允许您自动为您记录每个转换。要启用此行为,我们必须将 recordHistory() 设置为返回 true;

public function recordHistory(): bool
{
    return true;
}

注册我们的 StateMachine

一旦我们定义了 StateMachine,我们就可以在我们的 SalesOrder 模型中的 $stateMachine 属性中注册它。在这里,我们设置绑定模型 field 和将控制它的状态机类。

use Wlhrtr\StateMachine\Traits\HasStateMachines;
use App\StateMachines\StatusStateMachine;

class SalesOrder extends Model
{
    Use HasStateMachines;

    public $stateMachines = [
        'status' => StatusStateMachine::class
    ];
}

状态机方法

当在模型中注册 $stateMachines 时,每个状态字段都将有自己的自定义方法来与状态机交互以及转换方法。 HasStateMachines 特性为 $stateMachines 中映射的每个字段定义了一个方法。例如。

对于

'status' => StatusStateMachine::class,
'fulfillment_status' => FulfillmentStatusStateMachine::class

我们将有一个配套的方法

status();
fulfillment_status(); // or fulfillmentStatus()

我们可以使用该方法来检查我们的当前状态、历史记录并应用转换。

注意:字段 "status" 将保持完整并与状态机同步

状态转换

要从一个状态转换到另一个状态,我们可以使用 transitionTo 方法。例如

$salesOrder->status()->transitionTo($to = 'approved');

如果需要,您也可以传入 $customProperties

$salesOrder->status()->transitionTo($to = 'approved', $customProperties = [
    'comments' => 'All ready to go'
]);

还可以指定一个 $responsible。默认情况下,将使用 auth()->user()

$salesOrder->status()->transitionTo(
    $to = 'approved',
    $customProperties = [],
    $responsible = User::first()
);

在应用转换时,状态机将根据我们定义的 transitions() 状态验证状态转换是否允许。如果转换不允许,将抛出 TransitionNotAllowed 异常。

查询历史

如果我们的状态机中 recordHistory() 设置为 true,则每个状态转换都将记录在 StateHistory 模型中,使用在安装包时导出的 state_histories 表。

开启 recordHistory() 后,我们可以查询字段已转换到的状态历史。例如

$salesOrder->status()->was('approved'); // true or false

$salesOrder->status()->timesWas('approved'); // int

$salesOrder->status()->whenWas('approved'); // ?Carbon

如上所示,我们可以检查字段是否已转换到查询的任何状态。

我们还可以获取给定状态的最新快照或所有快照

$salesOrder->status()->snapshotWhen('approved');

$salesOrder->status()->snapshotsWhen('approved');

转换状态的完整历史记录也可用

$salesOrder->status()->history()->get();

history() 方法返回一个 Eloquent 关系,可以与以下作用域一起使用,以进一步缩小结果。

$salesOrder->status()->history()
    ->from('pending')
    ->to('approved')
    ->withCustomProperty('comments', 'like', '%good%')
    ->get();

使用查询构建器

HasStateMachines 特性在根据每个状态机的状态历史查询模型时引入了一个辅助方法。您可以使用 whereHas{FIELD_NAME}(例如:whereHasStatuswhereHasFulfillment)根据状态转换、负责人和自定义属性添加约束。

whereHas{FIELD_NAME} 方法接受一个闭包,您可以在其中添加以下类型的约束

  • withTransition($from, $to)
  • transitionedFrom($to)
  • transitionedTo($to)
  • withResponsible($responsible|$id)
  • withCustomProperty($property, $operator, $value)

参数 $from$to 可以是字符串或状态名称的数组。

SalesOrder::with()
    ->whereHasStatus(function ($query) {
        $query
            ->withTransition('pending', 'approved')
            ->withResponsible(auth()->id())
        ;
    })
    ->whereHasFulfillment(function ($query) {
        $query
            ->transitionedTo('complete')
        ;
    })
    ->get();

获取自定义属性

在应用带有自定义属性的转换时,我们可以使用 getCustomProperty($key) 方法获取我们的注册值。例如。

$salesOrder->status()->getCustomProperty('comments');

此方法将访问当前状态的自定义属性。您可以使用 snapshotWhen($state) 方法获取先前状态的自定义属性。

$salesOrder->status()->snapshotWhen('approved')->getCustomProperty('comments');

获取负责人

与自定义属性类似,您可以检索应用状态转换的 $responsible 对象。

$salesOrder->status()->responsible();

此方法将访问当前状态的责任人。您可以使用 snapshotWhen($state) 方法获取先前状态的责任人。

$salesOrder->status()->snapshotWhen('approved')->responsible;

注意:responsible 可以是 null,如果没有指定,并且在后台作业中发生转换时。这是因为没有可用的 auth()->user()

高级用法

跟踪属性更改

recordHistory() 激活时,模型状态转换记录在 state_histories 表中。每个转换记录都包含有关在状态转换期间更改的属性的信息。您可以通过 changedAttributesNames() 方法获取有关更改的信息。此方法将返回已更改的属性名称数组。使用这些属性名称,您可以使用 changedAttributeOldValue($attributeName)changedAttributeNewValue($attributeName) 方法分别获取旧值和新值。

$salesOrder = SalesOrder::create([
    'total' => 100,
]);

$salesOrder->total = 200;

$salesOrder->status()->transitionTo('approved');

$salesOrder->changedAttributesNames(); // ['total']

$salesOrder->changedAttributeOldValue('total'); // 100
$salesOrder->changedAttributeNewValue('total'); // 200

添加验证

在转换到新状态之前,我们可以添加允许/禁止转换的验证。为此,我们可以在状态机类中重写 validatorForTransition($from, $to, $model) 方法。

此方法必须返回一个 Validator,该 Validator 将在应用转换之前进行检查。如果验证器 fails(),则抛出 ValidationException。例如

use Illuminate\Support\Facades\Validator as ValidatorFacade;

class StatusStateMachine extends StateMachine
{
    public function validatorForTransition($from, $to, $model): ?Validator
    {
        if ($from === 'pending' && $to === 'approved') {
            return ValidatorFacade::make([
                'total' => $model->total,
            ], [
                'total' => 'gt:0',
            ]);
        }
        
        return parent::validatorForTransition($from, $to, $model);
    }
}

在上面的示例中,我们在应用转换之前验证我们的销售订单模型总金额是否大于 0。

添加钩子

我们还可以添加在应用转换之前/之后执行的自定义钩子/回调。为此,我们必须相应地重写状态机中的 beforeTransitionHooks()afterTransitionHooks() 方法。

两种转换钩子方法都必须返回一个键值数组,其中状态作为键,回调/闭包数组作为值。

注意:beforeTransitionHooks()的键必须是$from状态。注意:afterTransitionHooks()的键必须是$to状态。

示例

class StatusStateMachine extends StateMachine
{
    public function beforeTransitionHooks(): array
    {
        return [
            'approved' => [
                function ($to, $model) {
                    // Dispatch some job BEFORE "approved changes to $to"
                },
                function ($to, $model) {
                    // Send mail BEFORE "approved changes to $to"
                },
            ],
        ];
    }

    public function afterTransitionHooks(): array
    {
        return [
            'processed' => [
                function ($from, $model) {
                    // Dispatch some job AFTER "$from transitioned to processed"
                },
                function ($from, $model) {
                    // Send mail AFTER "$from transitioned to processed"
                },
            ],
        ];
    }
}

推迟转换

您还可以使用postponeTransitionTo方法推迟到其他状态的转换。此方法接受与transitionTo相同的参数,并接受一个$when Carbon实例来指定何时运行转换。

postponeTransitionTo不会立即应用转换。相反,它将其保存到pending_transitions表中,以便跟踪所有模型的所有待处理转换。

要启用稍后运行这些转换,您必须将PendingTransitionsDispatcher作业类安排到您的调度器中,每分钟、五分钟或十分钟运行一次。

$schedule->job(PendingTransitionsDispatcher::class)->everyMinute();

PendingTransitionsDispatcher负责在指定的$when日期/时间应用推迟的转换。

您可以使用hasPendingTransitions()方法检查模型是否具有特定状态机的待处理转换。

$salesOrder->status()->hasPendingTransitions();

测试

composer test

更改日志

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

贡献

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

安全性

如果您发现任何与安全相关的问题,请通过电子邮件santibanez.andres@gmail.com而不是使用问题跟踪器。

致谢

许可证

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