PHPackages                             iotron/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. iotron/laravel-state-machine

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

iotron/laravel-state-machine
============================

A robust, enum-aware state machine for Laravel Eloquent models

v1.0.0(4mo ago)3732↓89.5%[2 PRs](https://github.com/iotron/laravel-state-machine/pulls)MITPHPPHP ^8.2CI passing

Since Feb 7Pushed 2mo agoCompare

[ Source](https://github.com/iotron/laravel-state-machine)[ Packagist](https://packagist.org/packages/iotron/laravel-state-machine)[ Docs](https://iotron.co)[ GitHub Sponsors](https://github.com/sponsors/iotron)[ RSS](/packages/iotron-laravel-state-machine/feed)WikiDiscussions main Synced 2d ago

READMEChangelogDependencies (9)Versions (5)Used By (0)

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

[](#laravel-state-machine)

[![Latest Version on Packagist](https://camo.githubusercontent.com/907efdfd2c54e4385e47eb6687dece8f09095513bb9e099f389b2b0400bf6ec4/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f762f696f74726f6e2f6c61726176656c2d73746174652d6d616368696e652e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/iotron/laravel-state-machine)[![GitHub Tests Action Status](https://camo.githubusercontent.com/0d709d7f405140984fd301378fa7fd3891a38ba1e10e91a28ffbbe55f449d254/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f616374696f6e732f776f726b666c6f772f7374617475732f696f74726f6e2f6c61726176656c2d73746174652d6d616368696e652f72756e2d74657374732e796d6c3f6272616e63683d6d61696e266c6162656c3d7465737473267374796c653d666c61742d737175617265)](https://github.com/iotron/laravel-state-machine/actions?query=workflow%3Arun-tests+branch%3Amain)[![Total Downloads](https://camo.githubusercontent.com/575961255b8ddd0706421ee772d8fc13493e24c23160168881e54eabd788d8ee/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f64742f696f74726f6e2f6c61726176656c2d73746174652d6d616368696e652e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/iotron/laravel-state-machine)

A robust, enum-aware state machine for Laravel Eloquent models. Define allowed transitions, validate before state changes, track full history, and prevent N+1 queries — all with native PHP BackedEnum support and zero dependencies beyond Laravel.

Features
--------

[](#features)

- **Native BackedEnum support** — use enums everywhere, normalized internally
- **N+1 query prevention** — eager-load history, get zero-query lookups in loops
- **Transaction-safe transitions** — model save + history recording are atomic
- **Lifecycle events** — `TransitionStarted`, `TransitionCompleted`, `TransitionFailed`
- **Validation hooks** — block invalid transitions with Laravel Validator
- **Before/after hooks** — run closures on specific state changes
- **Pending transitions** — schedule future state changes with jobs
- **History tracking** — full audit trail with custom properties and changed attributes
- **Safe auth resolution** — no crashes in queue/CLI contexts
- **Artisan generator** — `php artisan make:state-machine`

Requirements
------------

[](#requirements)

- PHP 8.2+
- Laravel 11 or 12

Installation
------------

[](#installation)

```
composer require iotron/laravel-state-machine
```

Publish the config file:

```
php artisan vendor:publish --tag=state-machine-config
```

Publish and run the migrations:

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

> **Migrating from `asantibanez/laravel-eloquent-state-machines`?** See the [Migration Guide](#migrating-from-asantibanezlaravel-eloquent-state-machines) below — no database changes needed.

Quick Start
-----------

[](#quick-start)

### 1. Create a State Machine

[](#1-create-a-state-machine)

```
php artisan make:state-machine OrderStatusStateMachine
```

Define your transitions and default state:

```
namespace App\StateMachines;

use App\Enums\OrderStatus;
use Iotron\StateMachine\StateMachines\StateMachine;

class OrderStatusStateMachine extends StateMachine
{
    public function transitions(): array
    {
        return [
            'pending'    => ['confirmed', 'cancelled'],
            'confirmed'  => ['dispatched', 'cancelled'],
            'dispatched' => ['delivered'],
        ];
    }

    public function defaultState(): ?string
    {
        return OrderStatus::PENDING->value;
    }
}
```

### 2. Add the Trait to Your Model

[](#2-add-the-trait-to-your-model)

```
use Iotron\StateMachine\Concerns\HasStateMachines;

class Order extends Model
{
    use HasStateMachines;

    public $stateMachines = [
        'status' => OrderStatusStateMachine::class,
    ];

    protected function casts(): array
    {
        return [
            'status' => OrderStatus::class, // native enum cast works!
        ];
    }
}
```

### 3. Use It

[](#3-use-it)

```
$order = Order::create();

// Query state
$order->status()->is(OrderStatus::PENDING);       // true
$order->status()->canBe(OrderStatus::CONFIRMED);   // true
$order->status()->canBe(OrderStatus::DELIVERED);   // false

// Transition
$order->status()->transitionTo(OrderStatus::CONFIRMED);

// History
$order->status()->was(OrderStatus::PENDING);        // true
$order->status()->timesWas(OrderStatus::PENDING);    // 1
$order->status()->whenWas(OrderStatus::CONFIRMED);   // Carbon
$order->status()->snapshotWhen(OrderStatus::CONFIRMED); // Transition model

// Custom properties
$order->status()->transitionTo('dispatched', ['tracking' => 'ABC123']);
$order->status()->getCustomProperty('tracking'); // 'ABC123'
```

Configuration
-------------

[](#configuration)

```
// config/state-machine.php
return [
    'tables' => [
        'transitions'         => 'state_histories',    // history table name
        'pending_transitions' => 'pending_transitions',
    ],
    'record_changed_attributes'    => true,  // capture dirty attributes on transition
    'cancel_pending_on_transition' => true,  // auto-cancel pending when transitioning
];
```

Defining State Machines
-----------------------

[](#defining-state-machines)

### Transitions

[](#transitions)

The `transitions()` method returns a map of `from => [allowed targets]`:

```
public function transitions(): array
{
    return [
        'draft'     => ['pending', 'cancelled'],
        'pending'   => ['approved', 'rejected'],
        'approved'  => ['published'],
        // Wildcard support
        '*'         => ['archived'],  // any state can go to archived
    ];
}
```

### Default State

[](#default-state)

Set the initial state for new models:

```
public function defaultState(): ?string
{
    return 'draft';
    // or: return MyEnum::DRAFT->value;
}
```

### Record History

[](#record-history)

Control whether transitions are tracked (default: `true`):

```
public function recordHistory(): bool
{
    return true;
}
```

### Validation

[](#validation)

Return a `Validator` to block transitions that don't meet requirements:

```
use Illuminate\Contracts\Validation\Validator;
use Illuminate\Support\Facades\Validator as ValidatorFacade;

public function validatorForTransition($from, $to, $model): ?Validator
{
    if ($to === 'published') {
        $validator = ValidatorFacade::make([], []);

        if (! $model->title) {
            $validator->after(fn ($v) => $v->errors()->add(
                'title', 'A title is required before publishing.'
            ));
        }

        return $validator;
    }

    return null; // no validation for other transitions
}
```

If the validator fails, a `ValidationException` is thrown and the model stays unchanged.

### Before/After Hooks

[](#beforeafter-hooks)

Run closures when entering or leaving specific states. All hooks receive `($from, $to, $model)`:

```
public function beforeTransitionHooks(): array
{
    return [
        'published' => [ // keyed by the FROM state
            function (string $from, string $to, Model $model) {
                // Runs before leaving 'published'
            },
        ],
    ];
}

public function afterTransitionHooks(): array
{
    return [
        'confirmed' => [ // keyed by the TO state
            function (string $from, string $to, Model $model) {
                $model->update(['confirmed_at' => now()]);
            },
        ],
    ];
}
```

State Proxy API
---------------

[](#state-proxy-api)

Calling `$model->status()` returns a `State` proxy with these methods:

MethodReturnsDescription`state()``string`Current state (normalized to string)`is($state)``bool`Check if current state matches`isNot($state)``bool`Check if current state doesn't match`canBe($state)``bool`Check if transition is allowed`transitionTo($state, $props, $responsible)``void`Execute transition`postponeTransitionTo($state, $when, ...)``?PendingTransition`Schedule future transition`was($state)``bool`Ever been in this state?`timesWas($state)``int`Count times in this state`whenWas($state)``?Carbon`When last entered this state`snapshotWhen($state)``?Transition`Transition record for a state`snapshotsWhen($state)``Collection`All records for a state`history()``Builder`Query builder for this field's history`latest()``?Transition`Most recent transition to current state`getCustomProperty($key)``mixed`Custom property from latest transition`responsible()``?Model`User who triggered latest transition`allCustomProperties()``array`All custom properties from latest`pendingTransitions()``Builder`Query pending transitions`hasPendingTransitions()``bool`Any pending transitions?All methods accept both strings and `BackedEnum` values.

N+1 Prevention
--------------

[](#n1-prevention)

When you eager-load `stateHistory`, all history lookups use the in-memory collection — zero extra queries:

```
// 2 queries total: models + stateHistory
$orders = Order::with('stateHistory')->get();

// 0 additional queries for any number of models
foreach ($orders as $order) {
    $order->status()->was(OrderStatus::PENDING);
    $order->status()->timesWas(OrderStatus::CONFIRMED);
    $order->status()->snapshotWhen(OrderStatus::DISPATCHED);
}
```

Without eager loading, each call falls back to a database query automatically.

Events
------

[](#events)

Three events fire during transitions for app-wide listening:

EventWhenPayload`TransitionStarted`Before hooks fire`$model`, `$field`, `$from`, `$to``TransitionCompleted`After everything succeeds`$model`, `$field`, `$from`, `$to``TransitionFailed`On any exception`$model`, `$field`, `$from`, `$to`, `$exception````
// In a listener or EventServiceProvider
use Iotron\StateMachine\Events\TransitionCompleted;

Event::listen(TransitionCompleted::class, function (TransitionCompleted $event) {
    if ($event->field === 'status' && $event->to === 'published') {
        // Send notification, dispatch job, etc.
    }
});
```

Pending Transitions
-------------------

[](#pending-transitions)

Schedule transitions to execute in the future:

```
$order->status()->postponeTransitionTo('dispatched', Carbon::tomorrow());
```

Add the dispatcher job to your scheduler:

```
// bootstrap/app.php or routes/console.php
use Iotron\StateMachine\Jobs\DispatchPendingTransitions;

Schedule::job(new DispatchPendingTransitions)->everyMinute();
```

The job processes pending transitions in chunks and dispatches each as a separate queued job for reliability.

Transition Model
----------------

[](#transition-model)

The `Transition` model (stored in the `state_histories` table by default) includes useful scopes:

```
use Iotron\StateMachine\Models\Transition;

// Query scopes
Transition::forField('status')->to('published')->get();
Transition::withTransition('pending', 'published')->get();
Transition::withCustomProperty('reason', '=', 'approved')->get();
Transition::withResponsible($user)->get();

// Instance methods
$transition->getCustomProperty('key');
$transition->allCustomProperties();
$transition->changedAttributesNames();
$transition->changedAttributeOldValue('title');
$transition->changedAttributeNewValue('title');
```

Migrating from asantibanez/laravel-eloquent-state-machines
----------------------------------------------------------

[](#migrating-from-asantibanezlaravel-eloquent-state-machines)

This package is a drop-in replacement. No database migration needed — it reads the same `state_histories` table by default.

### Step 1: Install

[](#step-1-install)

```
composer require iotron/laravel-state-machine
```

### Step 2: Update model imports

[](#step-2-update-model-imports)

```
- use Asantibanez\LaravelEloquentStateMachines\Traits\HasStateMachines;
+ use Iotron\StateMachine\Concerns\HasStateMachines;
```

### Step 3: Update state machine base class

[](#step-3-update-state-machine-base-class)

```
- use Asantibanez\LaravelEloquentStateMachines\StateMachines\StateMachine;
+ use Iotron\StateMachine\StateMachines\StateMachine;
```

### Step 4: Update hook signatures

[](#step-4-update-hook-signatures)

The old package used `($from, $model)` / `($to, $model)`. This package uses a consistent `($from, $to, $model)` for both before and after hooks:

```
  public function afterTransitionHooks(): array
  {
      return [
          'confirmed' => [
-             function ($from, $model) {
+             function ($from, $to, $model) {
                  $model->update(['confirmed_at' => now()]);
              },
          ],
      ];
  }
```

### Step 5: Remove old packages

[](#step-5-remove-old-packages)

```
composer remove asantibanez/laravel-eloquent-state-machines javoscript/laravel-macroable-models
```

### Step 6: Enable native enum casts

[](#step-6-enable-native-enum-casts)

You can now use Laravel's native enum cast — no more workarounds:

```
protected function casts(): array
{
    return [
        'status' => OrderStatus::class, // just works!
    ];
}
```

### What's different?

[](#whats-different)

FeatureOld PackageThis PackageEnum supportManual workaroundsNative BackedEnumN+1 preventionNot built-inBuilt-in via eager loadingTransaction safetyNo wrapping`DB::transaction()`Hook argumentsInconsistent `($to, $model)` / `($from, $model)`Consistent `($from, $to, $model)`EventsNone3 lifecycle eventsAuth in queuesCrashesSafe fallbackDependenciesRequires `laravel-macroable-models`Zero external depsMethod resolutionStatic macros via reflectionNative `__call()`Testing
-------

[](#testing)

```
composer test
```

Credits
-------

[](#credits)

This package is inspired by and built upon the work of [asantibanez/laravel-eloquent-state-machines](https://github.com/asantibanez/laravel-eloquent-state-machines) by Andrés Santibáñez. The original package provided the foundation for Eloquent state machine management that this package extends with native BackedEnum support, N+1 prevention, transaction safety, lifecycle events, and other improvements.

Changelog
---------

[](#changelog)

Please see [CHANGELOG](CHANGELOG.md) for more information on what has changed recently.

License
-------

[](#license)

The MIT License (MIT). Please see [License File](LICENSE.md) for more information.

###  Health Score

42

—

FairBetter than 88% of packages

Maintenance81

Actively maintained with recent releases

Popularity21

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity50

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

Unknown

Total

1

Last Release

147d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/ebdc6d5603891cd03bd080ebbffaee9873241ee26aad154c89b14704f91dd7f7?d=identicon)[iotronlab](/maintainers/iotronlab)

---

Top Contributors

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

---

Tags

audit-traileloquentenumfinite-state-machinelaravellaravel-packagephpstate-machinetransitionsworkflowlaravelenumeloquentworkflowfinite-state machinestate-machineaudit-trailtransitionsiotron

###  Code Quality

TestsPest

Static AnalysisPHPStan

Code StyleLaravel Pint

### Embed Badge

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

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

###  Alternatives

[psalm/plugin-laravel

Psalm plugin for Laravel

3355.3M346](/packages/psalm-plugin-laravel)[laravel/ai

The official AI SDK for Laravel.

1.0k3.2M199](/packages/laravel-ai)[illuminate/database

The Illuminate Database package.

2.8k54.9M11.7k](/packages/illuminate-database)[watson/validating

Eloquent model validating trait.

9803.5M54](/packages/watson-validating)[api-platform/laravel

API Platform support for Laravel

58171.6k14](/packages/api-platform-laravel)[yajra/laravel-oci8

Oracle DB driver for Laravel via OCI8

8793.2M25](/packages/yajra-laravel-oci8)

PHPackages © 2026

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