norotaro/enumata

为 Eloquent 模型使用 Enum 实现状态机

v1.2.0 2024-06-06 16:02 UTC

This package is auto-updated.

Last update: 2024-09-06 16:25:57 UTC


README

Latest Version Tests Packagist PHP Version Packagist PHP Version

状态机 Eloquent 模型使用 Enum。

目录

描述

此包通过使用枚举文件表示所有可能的状态以及配置转换,以简单的方式实现 Eloquent 模型的状态机。

实时演示

您可以查看 norotaro/enumata-demo 仓库,或访问此 PHP 沙盒 中的演示的实时版本。

安装

composer require norotaro/enumata

基本用法

具有 status 字段和 4 个可能状态的模式

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

我们需要创建一个包含状态定义的 enum 文件,我们将其称为 OrderStatus。我们可以使用 make:model-state 命令来完成此操作

php artisan make:model-state OrderStatus

状态定义文件 - 枚举文件

上述命令将创建一个默认文件,我们可以根据需要对其进行修改

namespace App\Models;

use Norotaro\Enumata\Contracts\Nullable;
use Norotaro\Enumata\Contracts\DefineStates;

enum OrderStatus implements DefineStates
{
    case Pending;
    case Approved;
    case Declined;
    case Processed;

    public function transitions(): array
    {
        return match ($this) {
            // when the order is Pending we can approve() or decline() it
            self::Pending => [
                'approve' => self::Approved,
                'decline' => self::Delined,
            ],
            // when the order is Approved we can apply the processOrder() transition
            self::Approved => [
                'processOrder' => self::Processed,
            ],
        };
    }

    public static function default(): self
    {
        return self::Pending;
    }
}

transitions() 方法必须返回一个 key=>value 的数组,其中 key 是转换的名称,value 是在该转换中应用的状态。

请注意,默认情况下,将为每个转换在模型中创建方法。在示例中,将创建 approve()decline()processOrder() 方法。

配置模型

在模型中,我们必须注册 HasStateMachines 特性和然后在 $casts 属性中注册 enum 文件

use Norotaro\Enumata\Traits\HasStateMachines;

class Order extends Model
{
    use HasStateMachines;

    protected $casts = [
        'status' => OrderStatus::class,
    ];
}

就是这样!现在我们可以在状态之间转换。

访问当前状态

如果您访问属性,Eloquent 将返回具有当前状态的 enum 对象

$model = new Order;
$model->save();

$model->status; // App\Model\OrderStatus{name: "Pending"}
$model->fulfillment; // null

转换

默认情况下,此包将为 transitions() 返回的每个转换在模型中创建方法,因此,对于此示例,我们将具有以下方法可用

$model->approve(); // Change status to OrderStatus::Approved
$model->decline(); // Change status to OrderStatus::Declined
$model->processOrder(); // Change status to OrderStatus::Processed

禁用默认转换方法

您可以通过将模型的 $defaultTransitionMethods 属性设置为 false 来禁用转换方法的创建。

内部方法使用 StateMachine 类中可用的 transitionTo($state) 方法,因此您可以使用它来实现自定义转换方法。

class Order extends Model
{
    use HasStateMachines;

    // disable the creation of transition methods
    public bool $defaultTransitionMethods = false;

    protected $casts = [
        'status' => OrderStatus::class,
    ];

    // custom transition method
    public function myApproveTransition(): void {
        $this->status()->transitionTo(OrderStatus::Approved);
        //...
    }
}

转换不允许异常

如果应用了转换,并且当前状态不允许它,将抛出 TransitionNotAllowedException

$model->status; // App\Model\OrderStatus{name: "Pending"}
$model->processOrder(); // throws Norotaro\Enumata\Exceptions\TransitionNotAllowedException

强制转换

所有由特性和 transitionTo() 方法创建的转换方法都有 force 参数,当为 true 时,转换将应用而不检查定义的规则。

$model->status; // App\Model\OrderStatus{name: "Pending"}

$model->processOrder(force: true); // this will apply the transition and will not throw the exception

$model->status; // App\Model\OrderStatus{name: "Processed"}

$model->status()->transitionTo(OrderStatus::Pending, force:true); // will apply the transition without errors

可空状态

如果模型具有可空状态,我们只需在状态定义文件中实现 Norotaro\Enumata\Contracts\Nullable 合同即可。

例如,我们将向订单模型添加 fulfillment 属性

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

创建状态定义文件

我们可以使用带有 make:model-state 命令和 --nullable 选项的 make:model-state 命令创建枚举文件

php artisan make:model-state OrderFulfillment --nullable

编辑生成的文件后,我们可以得到类似以下内容

namespace App\Models;

use Norotaro\Enumata\Contracts\Nullable;
use Norotaro\Enumata\Contracts\DefineStates;

enum OrderFulfillment implements DefineStates, Nullable
{
    case Pending;
    case Completed;

    public function transitions(): array
    {
        return match ($this) {
            self::Pending => [
                'completeFulfillment' => self::Completed,
            ],
        };
    }

    public static function default(): ?self
    {
        return null;
    }

    public static function initialTransitions(): array
    {
        return [
            'initFulfillment' => self::Pending,
        ];
    }
}

initialTransitions() 方法必须返回字段为 null 时可用的转换列表。

transitions() 类似,默认情况下,方法将以 initialTransitions() 返回的键的名称创建。

注册状态定义文件

正如我们之前在 status 定义中做的那样,我们需要在 $casts 属性中注册该文件。

use Norotaro\Enumata\Traits\HasStateMachines;

class Order extends Model
{
    use HasStateMachines;

    protected $casts = [
        'status'      => OrderStatus::class,
        'fulfillment' => OrderFulfillment::class,
    ];
}

状态机

要访问状态机,我们只需要在属性名称后添加括号。

$model->status(); // Norotaro\Enumata\StateMachine

如果属性使用下划线,例如 my_attribute,您可以使用 my_attribute()myAttribute() 访问状态机。

使用状态机

转换

我们可以使用 transitionTo($state) 方法在状态之间进行转换。

$model->status()->transitionTo(OrderStatus::Approved);

检查可用转换

$model->status; // App\Model\OrderStatus{name: "Pending"}

$model->status()->canBe(OrderStatus::Approved); // true
$model->status()->canBe(OrderStatus::Processed); // false

事件

此包为默认由 Eloquent 分发的那些事件添加了两个新事件,并且可以以相同的方式使用。

有关 Eloquent 事件的更多信息,请参阅官方文档

  • transitioning:{attribute}:在保存到新状态之前,将分发此事件。
  • transitioned:{attribute}:在保存到新状态之后,将分发此事件。

transitioning 事件中,您可以按以下方式访问原始状态和新状态。

$from = $order->getOriginal('fulfillment'); // App\Model\OrderFulfillment{name: "Pending"}
$to   = $order->fulfillment; // App\Model\OrderFulfillment{name: "Complete"}

使用 $dispatchesEvents 监听事件

use App\Events\TransitionedOrderFulfillment;
use App\Events\TransitioningOrderStatus;
use Norotaro\Enumata\Traits\HasStateMachines;

class Order extends Model
{
    use HasStateMachines;

    protected $casts = [
        'status'      => OrderStatus::class,
        'fulfillment' => OrderFulfillment::class,
    ];

    protected $dispatchesEvents = [
        'transitioning:status'     => TransitioningOrderStatus::class,
        'transitioned:fulfillment' => TransitionedOrderFulfillment::class,
    ];
}

使用闭包监听事件

transitioning($field, $callback)transitioned($field, $callback) 方法有助于注册闭包。

请注意,第一个参数必须是我们要监听的字段的名称。

use Norotaro\Enumata\Traits\HasStateMachines;

class Order extends Model
{
    use HasStateMachines;

    protected $casts = [
        'status'      => OrderStatus::class,
        'fulfillment' => OrderFulfillment::class,
    ];

    protected static function booted(): void
    {
        static::transitioning('fulfillment', function (Order $order) {
            $from = $order->getOriginal('fulfillment');
            $to   = $order->fulfillment;

            \Log::debug('Transitioning fulfillment field', [
                'from' => $from->name,
                'to' => $to->name,
            ]);
        });

        static::transitioned('status', function (Order $order) {
            \Log::debug('Order status transitioned to ' . $order->status->name);
        });
    }
}

测试

要运行测试套件

composer run test

灵感

此包受到 asantibanez/laravel-eloquent-state-machines 的启发。

许可协议

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