asantibanez/laravel-eloquent-state-machines

为您的 Laravel Eloquent 模型提供状态机


README

Latest Version on Packagist Total Downloads

Laravel Eloquent State Machines

简介

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

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()->snapshotWhen('completed');

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

演示

您可以在此处查看演示和示例 这里

demo

安装

您可以通过 composer 安装此包

composer require asantibanez/laravel-eloquent-state-machines

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

php artisan vendor:publish --provider="Asantibanez\LaravelEloquentStateMachines\LaravelEloquentStateMachinesServiceProvider" --tag="migrations"

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

php artisan migrate

用法

定义我们的状态机

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

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

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

php artisan make:state-machine StatusStateMachine

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

use Asantibanez\LaravelEloquentStateMachines\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
}

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

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

注册我们的状态机

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

use Asantibanez\LaravelEloquentStateMachines\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,它将用于在应用转换之前检查转换。如果验证器 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

变更日志

请参阅 CHANGELOG 了解最近更改的更多信息。

贡献

请参阅 CONTRIBUTING 了解详细信息。

安全

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

鸣谢

许可证

MIT 许可证 (MIT)。请参阅 许可证文件 了解更多信息。