ahmedashraf093/better-eloquent-state-machine

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

v6.0.0 2023-11-17 16:11 UTC

This package is auto-updated.

Last update: 2024-09-23 10:06:15 UTC


README

Latest Version on Packagist Total Downloads

Eloquent State Machine

简介

此包提供了一种非常简单且易于使用的 API,可以可靠地在 eloquent 模型中管理状态机(模型的状态始终在变化),所有这些都在一个文件中完成。使用简单的单行命令来完成所有验证、记录和执行状态转换。

基于 asantibanez 的状态机 laravel-eloquent-state-machines 的作品

示例

具有两个状态字段的模型

$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

# php named args example
$salesOrder->status()->transitionTo(to: 'approved', responsible: auth()->user())

检查可用的转换

$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 ahmedashraf093/better-eloquent-state-machine

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

php artisan vendor:publish --provider="Ashraf\EloquentStateMachine\LaravelEloquentStateMachinesServiceProvider" --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 Ashraf\EloquentStateMachine\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' => fn($model, $who): bool => true, 
            'declined' => fn($model, $who): bool => $who->getTable() === User::tableName(), // only allow users to decline
        ],
        'approved' => [
            'processed' // no need for extra validation
        ],
    ];
}

允许通配符以允许任何状态变化

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 Ashraf\EloquentStateMachine\Traits\HasStateMachines;
use App\StateMachines\StatusStateMachine;

class SalesOrder extends Model
{
    Use HasStateMachines;

    /**
     *  mark the `status` to be controlled by `StatusStateMachine`
     */
    public $stateMachines = [
        'status' => StatusStateMachine::class
    ];
}

状态机方法

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

对于

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

我们将有一个相应的函数

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

我们可以用它来检查当前状态、历史记录以及应用转换。

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

状态转换

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

$salesOrder->status()->transitionTo($to = 'approved');
# PHP8 named args
$salesOrder->status()->transitionTo(to: 'approved');

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

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

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

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

在应用转换时,状态机将验证根据我们定义的transitions状态是否允许状态转换。如果不允许转换,将抛出Ashraf\EloquentStateMachine\Exceptions\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

添加验证

使用闭包函数

使用闭包函数在转换到下一个状态之前对每个状态进行验证。

以下是一个允许任何用户批准销售订单但只有用户可以拒绝的状态机的示例。

public function transitions(): array
{
    return [
        'pending' => [
            'approved' => fn($model, $who): bool => true, 
            'declined' => fn($model, $who): bool => $who->getTable() === User::tableName(), // only allow users to decline
            #             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
        ],
        'approved' => [
            'processed' // no need for extra validation
        ],
    ];
}

箭头函数

fn($model, $who): bool => $who->getTable() === User::tableName()

在转换到下一个状态之前将被调用,并将传递模型以及负责转换的对象。函数返回的 bool 值将决定转换是否允许。

一个更复杂的例子是只允许具有特定角色的用户批准销售订单。

public function transitions(): array
{
    return [
        'pending' => [
            'approved' => fn($model, $who): bool => $who->hasRole('sales_manager') && $who->can('approve', $model),
            'declined' => fn($model, $who): bool => $who->getTable() === User::tableName(), // only allow users to decline
        ],
        'approved' => [
            'processed' // no need for extra validation
        ],
    ];
}

获取可用的转换

使用 ->stateMachine->availableTransitions() 方法,您可以在应用所有验证后获取当前状态的所有可用的转换。

$salesOrder->status()->stateMachine->availableTransitions(); // ['approved', 'declined']

根据负责转换的对象,可以对相同的转换应用不同的验证。

$user = User::first();
$salesOrder->status()->stateMachine->availableTransitions($user); // ['approved']

添加钩子

我们还可以添加自定义钩子/回调函数,这些函数将在转换应用之前/之后执行。为此,我们必须相应地覆盖状态机中的 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"
                },
            ],
        ];
    }
}

测试

composer test

变更日志

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

贡献

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

安全

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

鸣谢

许可协议

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