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

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

abarbod/laravel-state-machine
=============================

A production-ready state machine package for Laravel Eloquent models

v0.1.2(4mo ago)00MITPHPPHP ^8.1

Since Jan 2Pushed 4mo agoCompare

[ Source](https://github.com/abarbod/laravel-state-machine)[ Packagist](https://packagist.org/packages/abarbod/laravel-state-machine)[ RSS](/packages/abarbod-laravel-state-machine/feed)WikiDiscussions main Synced 1mo ago

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

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

[](#laravel-state-machine)

A production-ready state machine package for Laravel Eloquent models. Enforce state transitions, prevent invalid state changes, and maintain a complete audit trail.

Features
--------

[](#features)

- **Formal State Machine**: Define states and transitions with a fluent API
- **Guard Conditions**: Prevent invalid transitions with guard callbacks
- **Hooks**: Execute code before, after, or on failure of transitions
- **History Tracking**: Complete audit trail of all state changes
- **Concurrency Safety**: Optimistic locking prevents race conditions
- **Strict Mode**: Prevent direct state assignment (must use transitions)
- **Multi-State Support**: Multiple independent state machines per model
- **Diagram Generation**: Visualize state machines as Mermaid or DOT diagrams
- **Testing Helpers**: Assertions for testing state transitions

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

[](#installation)

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

Publish the configuration file:

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

Run migrations:

```
php artisan migrate
```

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

[](#quick-start)

### 1. Define a State Machine

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

In your `AppServiceProvider` or a dedicated service provider:

```
use Laravel\StateMachine\Facades\StateMachine;
use App\Models\Order;

public function boot(): void
{
    StateMachine::define(Order::class, 'status', function ($machine) {
        $machine->states(['draft', 'paid', 'shipped', 'delivered', 'canceled'])
            ->initial('draft')
            ->transition('pay')
                ->from('draft')
                ->to('paid')
                ->guard(fn(Order $order) => $order->total > 0)
                ->after(fn(Order $order) => event(new OrderPaid($order)))
            ->transition('ship')
                ->from('paid')
                ->to('shipped')
                ->guard(fn(Order $order) => $order->items()->count() > 0)
            ->transition('deliver')
                ->from('shipped')
                ->to('delivered')
            ->transition('cancel')
                ->from(['draft', 'paid'])
                ->to('canceled');
    });
}
```

### 2. Use the Trait in Your Model

[](#2-use-the-trait-in-your-model)

```
use Illuminate\Database\Eloquent\Model;
use Laravel\StateMachine\HasStateMachine;

class Order extends Model
{
    use HasStateMachine;

    protected $fillable = ['status', 'total'];
}
```

### 3. Execute Transitions

[](#3-execute-transitions)

```
$order = Order::create(['status' => 'draft', 'total' => 100]);

// Check if transition is allowed
if ($order->canTransition('pay')) {
    $order->transition('pay');
}

// Or directly transition (throws exception if not allowed)
$order->transition('pay');
```

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

[](#configuration)

Edit `config/state-machine.php`:

```
return [
    'history' => env('STATE_MACHINE_HISTORY', true),
    'strict' => env('STATE_MACHINE_STRICT', false),
    'transactional' => env('STATE_MACHINE_TRANSACTIONAL', true),
    'actor_resolver' => fn() => auth()->id(),
    'concurrency' => [
        'enabled' => env('STATE_MACHINE_CONCURRENCY_ENABLED', true),
        'retry_attempts' => env('STATE_MACHINE_RETRY_ATTEMPTS', 3),
    ],
];
```

### Options

[](#options)

- **history**: Enable/disable transition history logging (default: `true`)
- **strict**: Prevent direct state assignment (default: `false`)
- **transactional**: Run transitions in database transactions (default: `true`)
- **actor\_resolver**: Callback to determine the user/actor for history (default: `auth()->id()`)
- **concurrency.enabled**: Enable optimistic locking (default: `true`)
- **concurrency.retry\_attempts**: Number of retry attempts on concurrent conflicts (default: `3`)

Advanced Usage
--------------

[](#advanced-usage)

### Guards

[](#guards)

Guards prevent transitions when conditions aren't met:

```
->transition('ship')
    ->from('paid')
    ->to('shipped')
    ->guard(fn(Order $order) => $order->items()->count() > 0)
    ->guard(fn(Order $order) => $order->shipping_address !== null)
```

If any guard returns `false` or throws an exception, the transition is prevented.

### Hooks

[](#hooks)

Execute code at different stages of a transition:

```
->transition('pay')
    ->from('draft')
    ->to('paid')
    ->before(function ($order, $from, $to, $transition, $context) {
        // Runs before state change (in transaction)
        logger("About to pay order {$order->id}");
    })
    ->after(function ($order, $from, $to, $transition, $context) {
        // Runs after successful state change (outside transaction)
        event(new OrderPaid($order));
    })
    ->onFailure(function ($order, $from, $to, $transition, $context) {
        // Runs if transition fails
        logger("Failed to pay order {$order->id}");
    })
```

Hook parameters:

- `$model`: The model instance
- `$from`: The current state
- `$to`: The target state
- `$transition`: The transition name
- `$context`: Additional context data

### Multiple Source States

[](#multiple-source-states)

Transitions can originate from multiple states:

```
->transition('cancel')
    ->from(['draft', 'paid', 'shipped'])
    ->to('canceled')
```

### Wildcard Transitions

[](#wildcard-transitions)

Allow transitions from any state:

```
->transition('reset')
    ->from('*')
    ->to('draft')
```

### Transition History

[](#transition-history)

Access the transition history:

```
$history = $order->getStateHistory();
// Returns collection of StateTransition models

// Or use the relationship
$transitions = $order->stateTransitions;
```

### Multiple State Machines

[](#multiple-state-machines)

Support multiple independent state machines per model:

```
// In your model
class Order extends Model
{
    use HasStateMachine;

    protected $stateMachineField = 'status'; // Default field

    // For multiple fields, define machines separately
}

// Define machines for different fields
StateMachine::define(Order::class, 'status', function ($machine) {
    // status state machine
});

StateMachine::define(Order::class, 'payment_status', function ($machine) {
    // payment_status state machine
});
```

### Strict Mode

[](#strict-mode)

Enable strict mode to prevent direct state assignment:

```
STATE_MACHINE_STRICT=true
```

With strict mode enabled:

```
// This will throw an exception:
$order->status = 'paid';

// Must use transitions:
$order->transition('pay');
```

Diagram Generation
------------------

[](#diagram-generation)

Generate visual diagrams of your state machines:

```
php artisan state-machine:diagram Order
php artisan state-machine:diagram Order --field=status
php artisan state-machine:diagram Order --format=dot --output=order.dot
php artisan state-machine:diagram Order --format=mermaid --output=order.md
```

Supported formats:

- `mermaid` (default): Mermaid flowchart syntax
- `dot`: Graphviz DOT format

Testing
-------

[](#testing)

The package includes testing helpers:

```
use Laravel\StateMachine\Tests\TestCase;

class OrderTest extends TestCase
{
    public function test_order_can_be_paid(): void
    {
        $order = Order::create(['status' => 'draft', 'total' => 100]);

        $this->assertCanTransition($order, 'pay');
        $this->assertCannotTransition($order, 'ship');

        $order->transition('pay');

        $this->assertState($order, 'paid');
        $this->assertTransitionHistory($order, ['pay']);
    }
}
```

Available assertions:

- `assertCanTransition($model, $transition)`: Assert transition is allowed
- `assertCannotTransition($model, $transition)`: Assert transition is not allowed
- `assertState($model, $expectedState, $field = null)`: Assert current state
- `assertTransitionHistory($model, $expectedTransitions, $field = null)`: Assert transition history

Error Handling
--------------

[](#error-handling)

The package throws specific exceptions:

- `StateMachineNotDefinedException`: No state machine defined for model/field
- `InvalidTransitionException`: Transition not allowed from current state
- `TransitionGuardFailedException`: Guard condition failed
- `ConcurrentTransitionException`: Concurrent state change detected

```
use Laravel\StateMachine\Exceptions\InvalidTransitionException;

try {
    $order->transition('invalid');
} catch (InvalidTransitionException $e) {
    // Handle invalid transition
}
```

Performance Considerations
--------------------------

[](#performance-considerations)

- State machine definitions are stored in memory (can be cached in production)
- Transition execution is O(1) - no reflection in hot paths
- Optimistic locking uses efficient WHERE clauses
- History logging is optional and can be disabled

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

[](#contributing)

Contributions are welcome! Please feel free to submit a Pull Request.

License
-------

[](#license)

The MIT License (MIT). Please see the license file for more information.

###  Health Score

30

—

LowBetter than 64% of packages

Maintenance76

Regular maintenance activity

Popularity0

Limited adoption so far

Community2

Small or concentrated contributor base

Maturity35

Early-stage or recently created project

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

3

Last Release

129d ago

### Community

Maintainers

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

---

Tags

laraveleloquentworkflowfsmstate-machine

###  Code Quality

TestsPHPUnit

### Embed Badge

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

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

###  Alternatives

[tucker-eric/eloquentfilter

An Eloquent way to filter Eloquent Models

1.8k4.8M26](/packages/tucker-eric-eloquentfilter)[watson/validating

Eloquent model validating trait.

9723.3M47](/packages/watson-validating)[cybercog/laravel-love

Make Laravel Eloquent models reactable with any type of emotions in a minutes!

1.2k302.7k1](/packages/cybercog-laravel-love)[cviebrock/eloquent-taggable

Easy ability to tag your Eloquent models in Laravel.

567694.8k3](/packages/cviebrock-eloquent-taggable)[clickbar/laravel-magellan

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

423715.4k1](/packages/clickbar-laravel-magellan)[reedware/laravel-relation-joins

Adds the ability to join on a relationship by name.

2121.2M13](/packages/reedware-laravel-relation-joins)

PHPackages © 2026

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