PHPackages                             webrek/laravel-state-machine - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Database &amp; ORM](/categories/database)
4. /
5. webrek/laravel-state-machine

ActiveLibrary[Database &amp; ORM](/categories/database)

webrek/laravel-state-machine
============================

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

v1.1.0(2d ago)00MITPHPPHP ^8.2CI passing

Since Jun 8Pushed 2d agoCompare

[ Source](https://github.com/webrek/laravel-state-machine)[ Packagist](https://packagist.org/packages/webrek/laravel-state-machine)[ Docs](https://github.com/webrek/laravel-state-machine)[ RSS](/packages/webrek-laravel-state-machine/feed)WikiDiscussions main Synced yesterday

READMEChangelog (2)Dependencies (9)Versions (3)Used By (0)

Laravel State Machine
=====================

[](#laravel-state-machine)

[![Latest Version on Packagist](https://camo.githubusercontent.com/10f704cc91748fbd33ef4e0e8613721a415b705cf5787344b9de3f20f31917e1/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f762f77656272656b2f6c61726176656c2d73746174652d6d616368696e652e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/webrek/laravel-state-machine)[![Total Downloads](https://camo.githubusercontent.com/a3862e1d249d6f3a20e93845eb650559d6dde8c3ceda298c7eb9cb1311b23c23/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f64742f77656272656b2f6c61726176656c2d73746174652d6d616368696e652e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/webrek/laravel-state-machine)[![Tests](https://camo.githubusercontent.com/5f9157ef79e3318d604fb505793ccbfa72f74e18be433347839b6bd07649dc4b/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f616374696f6e732f776f726b666c6f772f7374617475732f77656272656b2f6c61726176656c2d73746174652d6d616368696e652f74657374732e796d6c3f6272616e63683d6d61696e266c6162656c3d7465737473267374796c653d666c61742d737175617265)](https://github.com/webrek/laravel-state-machine/actions/workflows/tests.yml)[![PHP Version](https://camo.githubusercontent.com/064f0ccc5da50a2885fd0c6a3de3e80313c6171da7afebbb692943af596b3a0d/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f7068702d762f77656272656b2f6c61726176656c2d73746174652d6d616368696e652e7376673f7374796c653d666c61742d737175617265)](https://php.net)[![License](https://camo.githubusercontent.com/0318b4a1e4c6a6f92c3af5faebfb04c01cf08aca27875227f5bca60d27865aaa/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f6c2f77656272656b2f6c61726176656c2d73746174652d6d616368696e652e7376673f7374796c653d666c61742d737175617265)](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
----------

[](#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
----------------------------------------------

[](#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
---------------

[](#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
------

[](#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)
---------------------------

[](#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
-------

[](#diagram)

Render any machine as a [Mermaid](https://mermaid.js.org) 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
------

[](#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
------------------

[](#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
---------------------------

[](#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
------------

[](#requirements)

ComponentVersionPHP8.2+Laravel12.xTesting
-------

[](#testing)

```
composer install
composer test
```

Contributing
------------

[](#contributing)

See [CONTRIBUTING.md](CONTRIBUTING.md).

Security
--------

[](#security)

Please review the [security policy](SECURITY.md) before reporting a vulnerability.

License
-------

[](#license)

The MIT License (MIT). See [LICENSE](LICENSE).

###  Health Score

40

—

FairBetter than 86% of packages

Maintenance100

Actively maintained with recent releases

Popularity0

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity47

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~0 days

Total

2

Last Release

2d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/7d8deca81629993819087597b5ad7695976b02e3d014f038e26e985f35f569de?d=identicon)[webrek](/maintainers/webrek)

---

Top Contributors

[![webrek](https://avatars.githubusercontent.com/u/5001338?v=4)](https://github.com/webrek "webrek (4 commits)")

---

Tags

eloquentfsmlaravellaravel-packagephpstate-machineworkflowlaraveleloquentlaravel-packagestateworkflowtransitionstatusfsmstate-machineguard

###  Code Quality

TestsPHPUnit

Static AnalysisPHPStan

Code StyleLaravel Pint

Type Coverage Yes

### Embed Badge

![Health badge](/badges/webrek-laravel-state-machine/health.svg)

```
[![Health](https://phpackages.com/badges/webrek-laravel-state-machine/health.svg)](https://phpackages.com/packages/webrek-laravel-state-machine)
```

###  Alternatives

[psalm/plugin-laravel

Psalm plugin for Laravel

3325.1M337](/packages/psalm-plugin-laravel)[kirschbaum-development/eloquent-power-joins

The Laravel magic applied to joins.

1.6k29.9M42](/packages/kirschbaum-development-eloquent-power-joins)[watson/validating

Eloquent model validating trait.

9743.4M53](/packages/watson-validating)[laravel/ai

The official AI SDK for Laravel.

9782.1M153](/packages/laravel-ai)[reedware/laravel-relation-joins

Adds the ability to join on a relationship by name.

2121.2M16](/packages/reedware-laravel-relation-joins)[clickbar/laravel-magellan

This package provides functionality for working with the postgis extension in Laravel.

436834.4k1](/packages/clickbar-laravel-magellan)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
