bkfdev/eloquent-states-machine

Laravel Eloquent 模型的状态机

v1.1.2 2023-09-09 13:25 UTC

This package is auto-updated.

Last update: 2024-09-09 15:43:25 UTC


README

此包是从 (https://github.com/asantibanez/laravel-eloquent-state-machines) 克隆的,它允许您通过在特定的 StateMachine 类中定义转换逻辑来简化 Eloquent 模型状态的转换。每个类允许您注册验证、钩子和允许的转换和状态,使得每个 StateMachine 类成为从状态转换到下一个状态的唯一真理来源。

Laravel Eloquent State Machines 还允许您自动记录模型可能具有的所有状态的历史,并据此查询历史以采取特定操作。

该包的核心是创建一个简单但强大的 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();

安装

您可以通过 composer 安装此包

composer require bkfdev/eloquent-states-machine

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

php artisan vendor:publish --provider="Bkfdev\EloquentStatesMachine\EloquentStatesMachineServiceProvider" --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 Bkfdev\EloquentStatesMachine\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 Bkfdev\EloquentStatesMachine\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,则每个状态转换都会使用在安装包时导出的state_histories表记录在StateHistory模型中。

启用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,它将在应用转换之前进行检查。如果验证器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)。有关更多信息,请参阅 许可证文件