承接 webrek/laravel-state-machine 相关项目开发

从需求分析到上线部署,全程专人跟进,保证项目质量与交付效率

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

webrek/laravel-state-machine

Composer 安装命令:

composer require webrek/laravel-state-machine

包简介

Declarative state machines for Eloquent models with guards, events and transition history.

README 文档

README

Latest Version on Packagist Total Downloads Tests PHP Version License

Declarative state machines for Eloquent models. Define the states a model can be in and the transitions between them, then let the package enforce that only valid transitions happen — with guards, events and an optional audit trail.

Quickstart

composer require webrek/laravel-state-machine

Define a machine:

use Webrek\StateMachine\StateMachine;
use Webrek\StateMachine\Transition;

class OrderStatus extends StateMachine
{
    public function states(): array
    {
        return ['pending', 'paid', 'shipped', 'delivered', 'cancelled'];
    }

    public function transitions(): array
    {
        return [
            'pay'     => Transition::from('pending')->to('paid'),
            'ship'    => Transition::from('paid')->to('shipped')
                            ->guard(fn ($order) => filled($order->address)),
            'deliver' => Transition::from('shipped')->to('delivered'),
            'cancel'  => Transition::from(['pending', 'paid'])->to('cancelled'),
        ];
    }

    public function initialState(): string
    {
        return 'pending';
    }
}

Bind it to a model attribute:

use Illuminate\Database\Eloquent\Model;
use Webrek\StateMachine\Concerns\HasStateMachines;

class Order extends Model
{
    use HasStateMachines;

    public function stateMachines(): array
    {
        return ['status' => OrderStatus::class];
    }
}

Use it:

$order = Order::create();          // status seeded to "pending"

$order->stateMachine()->can('pay');        // true
$order->stateMachine()->allowed();         // ['pay', 'cancel']
$order->stateMachine()->apply('pay');      // status is now "paid", persisted

$order->stateMachine()->apply('deliver');  // throws TransitionNotAllowedException

Why a state machine instead of if statements

The status of an order, a subscription, a support ticket or a KYC application is rarely a free-form string — it's a set of named states with strict rules about which one can follow which. Encoding those rules as scattered if ($order->status === 'paid') checks means the rules live in a dozen places and nothing stops an invalid jump like pending → delivered.

A state machine puts the rules in one declaration and enforces them:

  • Invalid transitions can't happen. Applying a transition from the wrong state throws instead of silently corrupting your data.
  • Guards gate transitions on business rules. "You can't ship without an address" becomes a guard, not a code review comment.
  • Every change emits an event. Hook side effects (send the receipt, notify the warehouse) onto StateTransitioned instead of hunting for every setter.
  • You get a free audit trail. Optional history records who moved what, from where, to where, and when.

The handler API

$model->stateMachine($attribute) returns a handler. With a single machine the attribute is optional.

$sm = $order->stateMachine('status');

$sm->state();                  // 'paid'
$sm->is('paid');               // true
$sm->can('ship');              // bool — allowed from here AND guard passes
$sm->allowed();                // ['ship', ... ] transition names available now
$sm->canTransitionTo('shipped'); // bool
$sm->apply('ship', ['carrier' => 'DHL']); // returns the model
$sm->history();                // Collection of recorded transitions

The context array passed to apply() reaches guards and the dispatched events, and is stored with the history row.

Guards

A guard is a closure receiving the model and the context array. The transition is only allowed when it returns true.

'refund' => Transition::from('paid')->to('refunded')
    ->guard(fn ($order, array $context) => $context['approved_by'] ?? false),

can() returns false when a guard blocks; apply() throws GuardFailedException.

Transition effects (atomic)

Attach a side effect to a transition with ->using(). It runs inside the same database transaction as the state change and the history record, so the whole thing is all-or-nothing: if the effect throws, the state never moves.

'refund' => Transition::from('paid')->to('refunded')
    ->using(function ($order, array $context) {
        $order->payment->refund();          // if this throws...
        $order->refund_reference = $context['reference'];
        $order->save();
    }),

If payment->refund() throws, the transition rolls back — the order stays paid, no history row is written, and the in-memory model is reverted. No half-applied transitions.

Diagram

Render any machine as a Mermaid state diagram:

$order->stateMachine()->toMermaid();
// or, for a definition class:
(new OrderStatus)->toMermaid();
php artisan state-machine:diagram "App\\States\\OrderStatus"
stateDiagram-v2
    [*] --> pending
    pending --> paid: pay
    paid --> shipped: ship
    shipped --> delivered: deliver
    pending --> cancelled: cancel
    paid --> cancelled: cancel
Loading

Paste the output into a Markdown ```mermaid block (GitHub renders it) or any Mermaid live editor.

Events

Two events fire around every transition:

  • Webrek\StateMachine\Events\StateTransitioning — before the new state is saved.
  • Webrek\StateMachine\Events\StateTransitioned — after it is saved.

Both carry the model, attribute, from, to, transition name and context.

Event::listen(StateTransitioned::class, function ($event) {
    if ($event->transition === 'ship') {
        Notification::send($event->model->customer, new OrderShipped($event->model));
    }
});

Transition history

History is opt-in. Publish and run the migration, then enable it:

php artisan vendor:publish --tag=state-machine-migrations
php artisan migrate
STATE_MACHINE_HISTORY=true

Every applied transition is then recorded, and ->history() returns the trail, oldest first:

$order->stateMachine()->history()->each(function ($row) {
    echo "{$row->from_state}{$row->to_state} via {$row->transition}";
});

Each row stores the subject (morph), the field, from_state, to_state, the transition name, the JSON context and timestamps.

Multiple machines per model

A model can drive several attributes at once:

public function stateMachines(): array
{
    return [
        'status'         => OrderStatus::class,
        'payment_status' => PaymentStatus::class,
    ];
}

$order->stateMachine('payment_status')->apply('authorize');

Requirements

Component Version
PHP 8.2+
Laravel 12.x

Testing

composer install
composer test

Contributing

See CONTRIBUTING.md.

Security

Please review the security policy before reporting a vulnerability.

License

The MIT License (MIT). See LICENSE.

统计信息

  • 总下载量: 0
  • 月度下载量: 0
  • 日度下载量: 0
  • 收藏数: 0
  • 点击次数: 2
  • 依赖项目数: 0
  • 推荐数: 0

GitHub 信息

  • Stars: 0
  • Watchers: 0
  • Forks: 0
  • 开发语言: PHP

其他信息

  • 授权协议: MIT
  • 更新时间: 2026-06-08