PHPackages                             sharonthe1st/laravel-eloquent-state-machines - 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. sharonthe1st/laravel-eloquent-state-machines

ActiveLibrary

sharonthe1st/laravel-eloquent-state-machines
============================================

State machines for your Laravel Eloquent models

1.0(2y ago)03MITPHPPHP ^7.3|^7.4|^8.0

Since Mar 18Pushed 2y agoCompare

[ Source](https://github.com/SharonThe1st/laravel-eloquent-state-machines)[ Packagist](https://packagist.org/packages/sharonthe1st/laravel-eloquent-state-machines)[ Docs](https://github.com/asantibanez/laravel-eloquent-state-machines)[ RSS](/packages/sharonthe1st-laravel-eloquent-state-machines/feed)WikiDiscussions master Synced 1mo ago

READMEChangelog (1)Dependencies (5)Versions (2)Used By (0)

[![Latest Version on Packagist](https://camo.githubusercontent.com/680a176ada7f0d4b5f9b804612193fced71838f84731179b38f05da3204068ec/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f762f536861726f6e5468653173742f6c61726176656c2d656c6f7175656e742d73746174652d6d616368696e65732e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/SharonThe1st/laravel-eloquent-state-machines)[![Total Downloads](https://camo.githubusercontent.com/74f96b1c09f485c6ef74f330b33b5c49f73094cfa71fe693c43572fb92f4386b/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f64742f536861726f6e5468653173742f6c61726176656c2d656c6f7175656e742d73746174652d6d616368696e65732e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/SharonThe1st/laravel-eloquent-state-machines)

[![Laravel Eloquent State Machines](https://camo.githubusercontent.com/0e8a5ea28f879815564fe599932262762651c29f460ac9103848eba75e9d3f1a/68747470733a2f2f62616e6e6572732e6265796f6e64636f2e64652f4c61726176656c253230456c6f7175656e7425323053746174652532304d616368696e65732e706e673f7468656d653d6c69676874267061636b6167654d616e616765723d636f6d706f7365722b72657175697265267061636b6167654e616d653d6173616e746962616e657a2532466c61726176656c2d656c6f7175656e742d73746174652d6d616368696e6573267061747465726e3d63697263756974426f617264267374796c653d7374796c655f31266465736372697074696f6e3d53746174652b6d616368696e65732b666f722b796f75722b4c61726176656c2b456c6f7175656e742b6d6f64656c732b696e2b6e6f2b74696d65266d643d312673686f7757617465726d61726b3d3126666f6e7453697a653d313030707826696d616765733d636f6c6c656374696f6e)](https://camo.githubusercontent.com/0e8a5ea28f879815564fe599932262762651c29f460ac9103848eba75e9d3f1a/68747470733a2f2f62616e6e6572732e6265796f6e64636f2e64652f4c61726176656c253230456c6f7175656e7425323053746174652532304d616368696e65732e706e673f7468656d653d6c69676874267061636b6167654d616e616765723d636f6d706f7365722b72657175697265267061636b6167654e616d653d6173616e746962616e657a2532466c61726176656c2d656c6f7175656e742d73746174652d6d616368696e6573267061747465726e3d63697263756974426f617264267374796c653d7374796c655f31266465736372697074696f6e3d53746174652b6d616368696e65732b666f722b796f75722b4c61726176656c2b456c6f7175656e742b6d6f64656c732b696e2b6e6f2b74696d65266d643d312673686f7757617465726d61726b3d3126666f6e7453697a653d313030707826696d616765733d636f6c6c656374696f6e)

Introduction
------------

[](#introduction)

This package allows you to simplify the transitioning of states an Eloquent model could have by defining the transition logic in specific StateMachine classes. Each class allows you to register validations, hooks and allowed transitions and states making each StateMachine class the only source of truth when moving from a state to the next.

Laravel Eloquent State Machines also allow you to automatically record history of all states a model may have and query this history to take specific actions accordingly.

At its core, this package has been created to provide a simple but powerful API so Laravel developers feel right at home.

**Examples**

Model with two status fields

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

$salesOrder->fulfillment; // null, 'pending', 'completed'
```

Transitioning from one state to another

```
$salesOrder->status()->transitionTo('approved');

$salesOrder->fulfillment()->transitionTo('completed');

//With custom properties
$salesOrder->status()->transitionTo('approved', [
    'comments' => 'Customer has available credit',
]);

//With responsible
$salesOrder->status()->transitionTo('approved', [], $responsible); // auth()->user() by default
```

Checking available transitions

```
$salesOrder->status()->canBe('approved');

$salesOrder->status()->canBe('declined');
```

Checking current state

```
$salesOrder->status()->is('approved');

$salesOrder->status()->responsible(); // User|null
```

Checking transitions history

```
$salesOrder->status()->was('approved');

$salesOrder->status()->timesWas('approved');

$salesOrder->status()->whenWas('approved');

$salesOrder->fulfillment()->snapshowWhen('completed');

$salesOrder->status()->history()->get();
```

Demo
----

[](#demo)

You can check a demo and examples [here](https://github.com/SharonThe1st/laravel-eloquent-state-machines-demo)

[![demo](https://github.com/SharonThe1st/laravel-eloquent-state-machines/raw/master/demo.gif)](https://github.com/SharonThe1st/laravel-eloquent-state-machines/raw/master/demo.gif)

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

[](#installation)

You can install the package via composer:

```
composer require SharonThe1st/laravel-eloquent-state-machines
```

Next, you must export the package migrations

```
php artisan vendor:publish --provider="Asantibanez\LaravelEloquentStateMachines\LaravelEloquentStateMachinesServiceProvider" --tag="migrations"
```

Finally, prepare required database tables

```
php artisan migrate
```

Usage
-----

[](#usage)

### Defining our StateMachine

[](#defining-our-statemachine)

Imagine we have a `SalesOrder` model which has a `status` field for tracking the different stages our sales order can be in the system: `REGISTERED`, `APPROVED`, `PROCESSED` or `DECLINED`.

We can manage and centralize all of these stages and transitions within a StateMachine class. To define one, we can use the `php artisan make:state-machine` command.

For example, we can create a `StatusStateMachine` for our SalesOrder model

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

After running the command, we will have a new StateMachine class created in the `App\StateMachines` directory. The class will have the following code.

```
use Asantibanez\LaravelEloquentStateMachines\StateMachines\StateMachine;

class StatusStateMachine extends StateMachine
{
    public function recordHistory(): bool
    {
        return false;
    }

    public function transitions(): array
    {
        return [
            //
        ];
    }

    public function defaultState(): ?string
    {
        return null;
    }
}
```

Inside this class, we can define our states and allowed transitions

```
public function transitions(): array
{
    return [
        'pending' => ['approved', 'declined'],
        'approved' => ['processed'],
    ];
}
```

Wildcards are allowed to allow any state change

```
public function transitions(): array
{
    return [
        '*' => ['approved', 'declined'], // From any to 'approved' or 'declined'
        'approved' => '*', // From 'approved' to any
        '*' => '*', // From any to any
    ];
}
```

We can define the default/starting state too

```
public function defaultState(): ?string
{
    return 'pending'; // it can be null too
}
```

The StateMachine class allows recording each one of the transitions automatically for you. To enable this behavior, we must set `recordHistory()` to return `true`;

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

### Registering our StateMachine

[](#registering-our-statemachine)

Once we have defined our StateMachine, we can register it in our `SalesOrder` model, in a `$stateMachine`attribute. Here, we set the bound model `field` and state machine class that will control it.

```
use Asantibanez\LaravelEloquentStateMachines\Traits\HasStateMachines;
use App\StateMachines\StatusStateMachine;

class SalesOrder extends Model
{
    Use HasStateMachines;

    public $stateMachines = [
        'status' => StatusStateMachine::class
    ];
}
```

### State Machine Methods

[](#state-machine-methods)

When registering `$stateMachines` in our model, each state field will have it's own custom method to interact with the state machine and transitioning methods. The `HasStateMachines` trait defines one method per each field mapped in `$stateMachines`. Eg.

For

```
'status' => StatusStateMachine::class,
'fulfillment_status' => FulfillmentStatusStateMachine::class
```

We will have an accompanying method

```
status();
fulfillment_status(); // or fulfillmentStatus()
```

with which we can use to check our current state, history and apply transitions.

> Note: the field "status" will be kept intact and in sync with the state machine

### Transitioning States

[](#transitioning-states)

To transition from one state to another, we can use the `transitionTo` method. Eg:

```
$salesOrder->status()->transitionTo($to = 'approved');
```

You can also pass in `$customProperties` if needed

```
$salesOrder->status()->transitionTo($to = 'approved', $customProperties = [
    'comments' => 'All ready to go'
]);
```

A `$responsible` can be also specified. By default, `auth()->user()` will be used

```
$salesOrder->status()->transitionTo(
    $to = 'approved',
    $customProperties = [],
    $responsible = User::first()
);
```

When applying the transition, the state machine will verify if the state transition is allowed according to the `transitions()` states we've defined. If the transition is not allowed, a `TransitionNotAllowed`exception will be thrown.

### Querying History

[](#querying-history)

If `recordHistory()` is set to `true` in our State Machine, each state transition will be recorded in the package `StateHistory` model using the `state_histories` table that was exported when installing the package.

With `recordHistory()` turned on, we can query the history of states our field has transitioned to. Eg:

```
$salesOrder->status()->was('approved'); // true or false

$salesOrder->status()->timesWas('approved'); // int

$salesOrder->status()->whenWas('approved'); // ?Carbon
```

As seen above, we can check whether or not our field has transitioned to one of the queried states.

We can also get the latest snapshot or all snapshots for a given state

```
$salesOrder->status()->snapshotWhen('approved');

$salesOrder->status()->snapshotsWhen('approved');
```

The full history of transitioned states is also available

```
$salesOrder->status()->history()->get();
```

The `history()` method returns an Eloquent relationship that can be chained with the following scopes to further down the results.

```
$salesOrder->status()->history()
    ->from('pending')
    ->to('approved')
    ->withCustomProperty('comments', 'like', '%good%')
    ->get();
```

### Using Query Builder

[](#using-query-builder)

The `HasStateMachines` trait introduces a helper method when querying your models based on the state history of each state machine. You can use the `whereHas{FIELD_NAME}` (eg: `whereHasStatus`, `whereHasFulfillment`) to add constraints to your model queries depending on state transitions, responsible and custom properties.

The `whereHas{FIELD_NAME}` method accepts a closure where you can add the following type of constraints:

- `withTransition($from, $to)`
- `transitionedFrom($to)`
- `transitionedTo($to)`
- `withResponsible($responsible|$id)`
- `withCustomProperty($property, $operator, $value)`

The `$from` and `$to` parameters can be either a status name as a string or an array of status names.

```
SalesOrder::with()
    ->whereHasStatus(function ($query) {
        $query
            ->withTransition('pending', 'approved')
            ->withResponsible(auth()->id())
        ;
    })
    ->whereHasFulfillment(function ($query) {
        $query
            ->transitionedTo('complete')
        ;
    })
    ->get();
```

### Getting Custom Properties

[](#getting-custom-properties)

When applying transitions with custom properties, we can get our registered values using the `getCustomProperty($key)` method. Eg.

```
$salesOrder->status()->getCustomProperty('comments');
```

This method will reach for the custom properties of the current state. You can get custom properties of previous states using the snapshotWhen($state) method.

```
$salesOrder->status()->snapshotWhen('approved')->getCustomProperty('comments');
```

### Getting Responsible

[](#getting-responsible)

Similar to custom properties, you can retrieve the `$responsible` object that applied the state transition.

```
$salesOrder->status()->responsible();
```

This method will reach for the responsible of the current state. You can get responsible of previous states using the snapshotWhen($state) method.

```
$salesOrder->status()->snapshotWhen('approved')->responsible;
```

> Note: `responsible` can be `null` if not specified and when the transition happens in a background job. This is because no `auth()->user()` is available.

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

[](#advanced-usage)

### Tracking Attribute Changes

[](#tracking-attribute-changes)

When `recordHistory()` is active, model state transitions are recorded in the `state_histories` table. Each transition record contains information about the attributes that changed during the state transition. You can get information about what has changed via the `changedAttributesNames()` method. This method will return an array of the attributes names that changed. With these attributes names, you can then use the methods `changedAttributeOldValue($attributeName)`and `changedAttributeNewValue($attributeName)` to get the old and new values respectively.

```
$salesOrder = SalesOrder::create([
    'total' => 100,
]);

$salesOrder->total = 200;

$salesOrder->status()->transitionTo('approved');

$salesOrder->changedAttributesNames(); // ['total']

$salesOrder->changedAttributeOldValue('total'); // 100
$salesOrder->changedAttributeNewValue('total'); // 200
```

### Adding Validations

[](#adding-validations)

Before transitioning to a new state, we can add validations that will allow/disallow the transition. To do so, we can override the `validatorForTransition($from, $to, $model)` method in our state machine class.

This method must return a `Validator` that will be used to check the transition before applying it. If the validator `fails()`, a `ValidationException` is thrown. Eg:

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

class StatusStateMachine extends StateMachine
{
    public function validatorForTransition($from, $to, $model): ?Validator
    {
        if ($from === 'pending' && $to === 'approved') {
            return ValidatorFacade::make([
                'total' => $model->total,
            ], [
                'total' => 'gt:0',
            ]);
        }

        return parent::validatorForTransition($from, $to, $model);
    }
}
```

In the example above, we are validating that our Sales Order model total is greater than 0 before applying the transition.

### Adding Hooks

[](#adding-hooks)

We can also add custom hooks/callbacks that will be executed before/after a transition is applied. To do so, we must override the `beforeTransitionHooks()` and `afterTransitionHooks()` methods in our state machine accordingly.

Both transition hooks methods must return a keyed array with the state as key, and an array of callbacks/closures to be executed.

> NOTE: The keys for beforeTransitionHooks() must be the `$from` states. NOTE: The keys for afterTransitionHooks() must be the `$to` states.

Example

```
class StatusStateMachine extends StateMachine
{
    public function beforeTransitionHooks(): array
    {
        return [
            'approved' => [
                function ($to, $model) {
                    // Dispatch some job BEFORE "approved changes to $to"
                },
                function ($to, $model) {
                    // Send mail BEFORE "approved changes to $to"
                },
            ],
        ];
    }

    public function afterTransitionHooks(): array
    {
        return [
            'processed' => [
                function ($from, $model) {
                    // Dispatch some job AFTER "$from transitioned to processed"
                },
                function ($from, $model) {
                    // Send mail AFTER "$from transitioned to processed"
                },
            ],
        ];
    }
}
```

### Postponing Transitions

[](#postponing-transitions)

You can also postpone transitions to other states by using the `postponeTransitionTo` method. This method accepts the same parameters as `transitionTo` plus a `$when` Carbon instance to specify when the transition is to be run.

`postponeTransitionTo` doesn't apply the transition immediately. Instead, it saves it into a `pending_transitions` table where it keeps track of all pending transitions for all models.

To enable running this transitions at a later time, you must schedule the `PendingTransitionsDispatcher` job class into your scheduler to run every one, five or ten minutes.

```
$schedule->job(PendingTransitionsDispatcher::class)->everyMinute();
```

`PendingTransitionsDispatcher` is responsible for applying the postponed transitions at the specified `$when` date/time.

You can check if a model has pending transitions for a particular state machine using the `hasPendingTransitions()` method

```
$salesOrder->status()->hasPendingTransitions();
```

### Testing

[](#testing)

```
composer test
```

### Changelog

[](#changelog)

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

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

[](#contributing)

Please see [CONTRIBUTING](CONTRIBUTING.md) for details.

### Security

[](#security)

If you discover any security related issues, please email  instead of using the issue tracker.

Credits
-------

[](#credits)

- [Andrés Santibáñez](https://github.com/asantibanez)
- [All Contributors](../../contributors)

License
-------

[](#license)

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

###  Health Score

22

—

LowBetter than 22% of packages

Maintenance20

Infrequent updates — may be unmaintained

Popularity3

Limited adoption so far

Community12

Small or concentrated contributor base

Maturity46

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 81.1% 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

787d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/1ea7cb0ab2d0fed950858c63c439f2a63dcf5a0dfc505da6a166546d0ef92977?d=identicon)[sharonthe1st](/maintainers/sharonthe1st)

---

Top Contributors

[![asantibanez](https://avatars.githubusercontent.com/u/5126648?v=4)](https://github.com/asantibanez "asantibanez (73 commits)")[![SharonThe1st](https://avatars.githubusercontent.com/u/141559501?v=4)](https://github.com/SharonThe1st "SharonThe1st (6 commits)")[![jezzdk](https://avatars.githubusercontent.com/u/139632?v=4)](https://github.com/jezzdk "jezzdk (5 commits)")[![ajaxray](https://avatars.githubusercontent.com/u/439612?v=4)](https://github.com/ajaxray "ajaxray (2 commits)")[![leohubert](https://avatars.githubusercontent.com/u/13404544?v=4)](https://github.com/leohubert "leohubert (2 commits)")[![laravel-shift](https://avatars.githubusercontent.com/u/15991828?v=4)](https://github.com/laravel-shift "laravel-shift (1 commits)")[![j-dohnalek](https://avatars.githubusercontent.com/u/6439903?v=4)](https://github.com/j-dohnalek "j-dohnalek (1 commits)")

---

Tags

asantibanezlaravel-eloquent-state-machines

###  Code Quality

TestsPHPUnit

### Embed Badge

![Health badge](/badges/sharonthe1st-laravel-eloquent-state-machines/health.svg)

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

###  Alternatives

[asantibanez/laravel-eloquent-state-machines

State machines for your Laravel Eloquent models

568592.8k](/packages/asantibanez-laravel-eloquent-state-machines)[asantibanez/livewire-charts

Livewire Charts

896960.0k5](/packages/asantibanez-livewire-charts)[asantibanez/livewire-calendar

Laravel Livewire calendar component

96883.3k1](/packages/asantibanez-livewire-calendar)[asantibanez/livewire-select

Livewire dropdown select component

520138.4k2](/packages/asantibanez-livewire-select)[asantibanez/laravel-blade-sortable

Custom Blade components to add sortable/drag-and-drop HTML elements in your apps

41152.2k](/packages/asantibanez-laravel-blade-sortable)[asantibanez/livewire-status-board

Livewire component to show models/data according to its current status

34115.4k](/packages/asantibanez-livewire-status-board)

PHPackages © 2026

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