norotaro/enumata 问题修复 & 功能扩展

解决BUG、新增功能、兼容多环境部署,快速响应你的开发需求

邮箱:yvsm@zunyunkeji.com | QQ:316430983 | 微信:yvsm316

norotaro/enumata

Composer 安装命令:

composer require norotaro/enumata

包简介

State machines for Eloquent models with Enums

README 文档

README

Latest Version Tests Packagist PHP Version Packagist Laravel Version

State Machines for Eloquent models using Enums.

Table of Contents

Description

This package helps to implement State Machines to Eloquent models in an easy way using Enum files to represent all possible states and also to configure transitions.

Live demo

You can check the norotaro/enumata-demo repository or go to the live version of the demo in this PHP Sandbox.

Installation

composer require norotaro/enumata

Basic usage

Having a model with a status field and 4 possible states:

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

We need to create an enum file with the State Definitions which we will call OrderStatus. We can do this with the make:model-state command:

php artisan make:model-state OrderStatus

The State Definition file - enum file

The above command will create a default file that we can adapt to meet our needs:

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;
    }
}

The transitions() method must return an array with key=>value where the key is the name of the transition and the value is the state to apply in that transition.

Note that, by default, methods will be created in the model for each transition. In the case of the example, the approve(), decline(), and processOrder() methods will be created.

Configuring the model

In the model we have to implement the contract HasStateMachine and register the EloquentHasStateMachines trait and then the enum file in the $casts property:

use Norotaro\Enumata\Contracts\HasStateMachine;
use Norotaro\Enumata\Traits\EloquentHasStateMachines;

class Order extends Model implements HasStateMachine
{
    use EloquentHasStateMachines;

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

That's it! Now we can transition between the states.

Access the current state

If you access the attributes, Eloquent will return the enum object with the current state:

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

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

Transitioning

By default this package will create methods in the model for each transition returned by transitions() so, for this example, we will have these methods available:

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

Disable default transition methods

You can disable the creation of transition methods by making the $defaultTransitionMethods attribute of the model false.

Internally these methods use the transitionTo($state) method available in the StateMachine class, so you can implement your custom transition methods with it.

use Norotaro\Enumata\Contracts\HasStateMachine;
use Norotaro\Enumata\Traits\EloquentHasStateMachines;

class Order extends Model implements HasStateMachine
{
    use EloquentHasStateMachines;

    // 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);
        //...
    }
}

Transition not allowed exception

If a transition is applied and the current state does not allow it, the TransitionNotAllowedException will be thrown.

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

Force transitions

All the methods of transitions created by the trait and also the transitionTo() method have the force parameter which, when true, the transition is applied without checking the defined rules.

$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

Nullable States

If the model has nullable states we only have to implement the Norotaro\Enumata\Contracts\Nullable contract in the State Definition file.

As an example, we will add the fulfillment attribute to the Order model:

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

Create a State Definition file

We can create the enum file with the make:model-state command and the --nullable option:

php artisan make:model-state OrderFulfillment --nullable

After editing the generated file we can have something like this:

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,
        ];
    }
}

The initialTransitions() method must return the list of available transitions when the field is null.

As with transitions(), by default methods will be created with the name of the keys returned by initialTransitions().

Register the State Definition file

As we previously did with the status definition, we need to register the file in the $casts property:

use Norotaro\Enumata\Contracts\HasStateMachine;
use Norotaro\Enumata\Traits\EloquentHasStateMachines;

class Order extends Model implements HasStateMachine
{
    use EloquentHasStateMachines;

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

The State Machine

To access the State Machine we only need to add parentheses to the attribute name:

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

If the attribute uses underscore such as my_attribute, you can access the state machine using the camel case name of the attribute, myAttribute() in this case.

Using the State Machine

Transitioning

We can transition between states with the transitionTo($state) method:

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

Checking available transitions

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

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

Events

This package adds two new events to those dispatched by Eloquent by default and can be used in the same way.

More information about Eloquent Events can be found in the official documentation.

  • transitioning:{attribute}: This event is dispatched before saving the transition to a new state.
  • transitioned:{attribute}: This event is dispatched after saving the transition to a new state.

In the transitioning event you can access the original and the new state in this way:

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

Listening to events using $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,
    ];
}

Listening to events using Closures

The transitioning($field, $callback) and transitioned($field, $callback) methods help to register closures.

Note that the first parameter must be the name of the field we want to listen to.

use Norotaro\Enumata\Contracts\HasStateMachine;
use Norotaro\Enumata\Traits\EloquentHasStateMachines;

class Order extends Model implements HasStateMachine
{
    use EloquentHasStateMachines;

    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);
        });
    }
}

Testing

To run the test suite:

composer run test

Inspiration

This package was inspired by asantibanez/laravel-eloquent-state-machines.

LICENSE

The MIT License (MIT). Please see License File for more information.

norotaro/enumata 适用场景与选型建议

norotaro/enumata 是一款 基于 PHP 开发的 Composer 扩展包,目前已累计 3.27k 次下载、GitHub Stars 达 3, 最近一次更新时间为 2023 年 07 月 23 日, 在 PHP 生态内属于活跃度较高的组件。

它主要适用于以下技术方向: 「laravel」 「eloquent」 「state machines」 「finite-state machines」 「finite automaton」 「finite automata」 等业务场景。在实际项目中,围绕这些方向常见需要落地的问题包括:接口对接、性能调优、并发安全、与既有框架(Laravel / ThinkPHP / Yii / Webman 等)的兼容适配,以及生产环境的日志埋点与稳定性保障。

我们在过去多个企业项目中使用过 norotaro/enumata 或与其功能相近的方案,如果你在选型或落地过程中遇到问题,例如 版本兼容、二次改造、私有化封装、与内部系统对接、生产 BUG 排查,欢迎联系我们协助评估。

围绕 norotaro/enumata 我们能提供哪些服务?
定制开发 / 二次开发

基于 norotaro/enumata 在你已有业务上做功能扩展、字段裁剪、UI 适配、与内部账号 / 权限 / 日志系统的深度对接。

BUG 修复 & 性能优化

线上偶发问题、内存泄漏、慢查询、并发异常等排查修复;针对高流量场景做缓存、队列、索引层面的调优。

项目外包 & 长期维护

承接完整的项目从需求 → 设计 → 开发 → 上线 → 长期运维;也可按月提供技术保姆服务。

yvsm@zunyunkeji.com QQ:316430983 微信:yvsm316 西安尊云信息科技 · 专注 PHP / Go / 分布式系统研发

统计信息

  • 总下载量: 3.27k
  • 月度下载量: 0
  • 日度下载量: 0
  • 收藏数: 4
  • 点击次数: 23
  • 依赖项目数: 1
  • 推荐数: 0

GitHub 信息

  • Stars: 3
  • Watchers: 1
  • Forks: 1
  • 开发语言: PHP

其他信息

  • 授权协议: MIT
  • 更新时间: 2023-07-23