PHPackages                             ahmedashraf093/better-eloquent-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. ahmedashraf093/better-eloquent-state-machine

ActiveLibrary

ahmedashraf093/better-eloquent-state-machine
============================================

State machines for your Laravel Eloquent models

v6.0.2(9mo ago)22.4kMITPHPPHP ^7.3|^7.4|^8.0CI passing

Since Nov 17Pushed 9mo agoCompare

[ Source](https://github.com/ahmedashraf093/better-eloquent-state-machine)[ Packagist](https://packagist.org/packages/ahmedashraf093/better-eloquent-state-machine)[ Docs](https://github.com/ahmedashraf093/better-eloquent-state-machine)[ RSS](/packages/ahmedashraf093-better-eloquent-state-machine/feed)WikiDiscussions master Synced 1mo ago

READMEChangelog (3)Dependencies (5)Versions (4)Used By (0)

[![Latest Version on Packagist](https://camo.githubusercontent.com/10d4b638b387c4158f4394594764ba24df750ec7f07441de3d5a54410888c484/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f762f61686d65646173687261663039332f6265747465722d656c6f7175656e742d73746174652d6d616368696e652e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/ahmedashraf093/better-eloquent-state-machine)[![Total Downloads](https://camo.githubusercontent.com/1099d9e10607c0cad53811679392c15bad83107921deec65d0eb949b27ca5b1c/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f64742f61686d65646173687261663039332f6265747465722d656c6f7175656e742d73746174652d6d616368696e652e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/ahmedashraf093/better-eloquent-state-machine)

[![Eloquent State Machine](https://camo.githubusercontent.com/f8273d7d3ac3fe1dea322e36f706a984e8740642a60a03e4ea8d06c30cac3464/68747470733a2f2f62616e6e6572732e6265796f6e64636f2e64652f456c6f7175656e7425323053746174652532304d616368696e652e6a7065673f7468656d653d6461726b267061636b6167654d616e616765723d636f6d706f7365722b72657175697265267061636b6167654e616d653d61686d65646173687261663039332532466c61726176656c2d656c6f7175656e742d73746174652d6d616368696e65267061747465726e3d63697263756974426f617264267374796c653d7374796c655f32266465736372697074696f6e3d412b6265747465722b73746174652b6d616368696e652b666f722b796f75722b656c6f7175656e742b6d6f64656c732b776974682b737461746573266d643d312673686f7757617465726d61726b3d3126666f6e7453697a653d313030707826696d616765733d68747470732533412532462532466c61726176656c2e636f6d253246696d672532466c6f676f6d61726b2e6d696e2e737667)](https://camo.githubusercontent.com/f8273d7d3ac3fe1dea322e36f706a984e8740642a60a03e4ea8d06c30cac3464/68747470733a2f2f62616e6e6572732e6265796f6e64636f2e64652f456c6f7175656e7425323053746174652532304d616368696e652e6a7065673f7468656d653d6461726b267061636b6167654d616e616765723d636f6d706f7365722b72657175697265267061636b6167654e616d653d61686d65646173687261663039332532466c61726176656c2d656c6f7175656e742d73746174652d6d616368696e65267061747465726e3d63697263756974426f617264267374796c653d7374796c655f32266465736372697074696f6e3d412b6265747465722b73746174652b6d616368696e652b666f722b796f75722b656c6f7175656e742b6d6f64656c732b776974682b737461746573266d643d312673686f7757617465726d61726b3d3126666f6e7453697a653d313030707826696d616765733d68747470732533412532462532466c61726176656c2e636f6d253246696d672532466c6f676f6d61726b2e6d696e2e737667)

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

[](#introduction)

This package provides a very simple and easy API to reliably manage your [state machines](https://en.wikipedia.org/wiki/Finite-state_machine) (the ever changing state of your models) within your eloquent models all in one file. using a simple one liners to do all the validation, logging and execution of your state transitions.

> based on the work of [asantibanez](https://github.com/asantibanez)'s state machine [laravel-eloquent-state-machines](https://github.com/asantibanez/laravel-eloquent-state-machines)

**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

# php named args example
$salesOrder->status()->transitionTo(to: 'approved', responsible: auth()->user())
```

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();
```

Features
--------

[](#features)

- Define your state machines in a single file
- Define your state machines with states and allowed transitions
- Allow wildcards to allow any state change
- Allow custom properties to be saved with each transition
- Allow responsible to be saved with each transition
- Allow to record history of state transitions
- Allow to query models based on state transitions
- Allow to validate state transitions
- Allow to add hooks/callbacks before/after state transitions

Demo
----

[](#demo)

You can check a demo and examples [here](https://github.com/ahmedashraf093/better-eloquent-state-machine-demo)

[![demo](https://github.com/ahmedashraf093/better-eloquent-state-machine/raw/master/demo.gif)](https://github.com/ahmedashraf093/better-eloquent-state-machine/raw/master/demo.gif)

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

[](#installation)

You can install the package via composer:

```
composer require ahmedashraf093/better-eloquent-state-machine
```

Next, you must export the package migrations

```
php artisan vendor:publish --provider="Ashraf\EloquentStateMachine\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 Ashraf\EloquentStateMachine\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' => fn($model, $who): bool => true,
            'declined' => fn($model, $who): bool => $who->getTable() === User::tableName(), // only allow users to decline
        ],
        'approved' => [
            'processed' // no need for extra validation
        ],
    ];
}
```

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 Ashraf\EloquentStateMachine\Traits\HasStateMachines;
use App\StateMachines\StatusStateMachine;

class SalesOrder extends Model
{
    Use HasStateMachines;

    /**
     *  mark the `status` to be controlled by `StatusStateMachine`
     */
    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');
```

```
# PHP8 named args
$salesOrder->status()->transitionTo(to: 'approved');
```

You can also pass in `$customProperties` if needed

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

```
# PHP8 named args
$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()
);
```

```
# PHP8 named args
$salesOrder->status()->transitionTo(
    to: 'approved',
    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 `Ashraf\EloquentStateMachine\Exceptions\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)

#### Using closure functions

[](#using-closure-functions)

Using closure function to do per state validation before transitioning to the next state.

here is an example of a state machine that allows any user to approve a sales order but only users can decline it.

```
public function transitions(): array
{
    return [
        'pending' => [
            'approved' => fn($model, $who): bool => true,
            'declined' => fn($model, $who): bool => $who->getTable() === User::tableName(), // only allow users to decline
            #             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
        ],
        'approved' => [
            'processed' // no need for extra validation
        ],
    ];
}
```

the arrow function

```
fn($model, $who): bool => $who->getTable() === User::tableName()
```

will be called before transitioning to the next state and will be passed the model and the responsible for the transition. the `bool` value returned by the function will determine if the transition will be allowed or not.

a more complex example would be to allow only users with a specific role to approve a sales order.

```
public function transitions(): array
{
    return [
        'pending' => [
            'approved' => fn($model, $who): bool => $who->hasRole('sales_manager') && $who->can('approve', $model),
            'declined' => fn($model, $who): bool => $who->getTable() === User::tableName(), // only allow users to decline
        ],
        'approved' => [
            'processed' // no need for extra validation
        ],
    ];
}
```

### Get available transitions

[](#get-available-transitions)

using the `->stateMachine->availableTransitions()` method you can get all the available transitions from the current state with all validation applied.

```
$salesOrder->status()->stateMachine->availableTransitions(); // ['approved', 'declined']
```

different validations can be applied to the same transition depending on the responsible for the transition.

```
$user = User::first();
$salesOrder->status()->stateMachine->availableTransitions($user); // ['approved']
```

### 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"
                },
            ],
        ];
    }
}
```

### 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)

- [Ahmed Ashraf](https://github.com/ahmedashraf093)
- [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

36

—

LowBetter than 82% of packages

Maintenance57

Moderate activity, may be stable

Popularity19

Limited adoption so far

Community12

Small or concentrated contributor base

Maturity49

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 78.5% 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 ~313 days

Total

3

Last Release

287d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/8cec388fa5253d9de96aa2147e7fb43d0eca6af1ff8b63bb7d8c6968c5351d2b?d=identicon)[ahmedashraf093](/maintainers/ahmedashraf093)

---

Top Contributors

[![asantibanez](https://avatars.githubusercontent.com/u/5126648?v=4)](https://github.com/asantibanez "asantibanez (73 commits)")[![ahmedashraf093](https://avatars.githubusercontent.com/u/118922563?v=4)](https://github.com/ahmedashraf093 "ahmedashraf093 (9 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)")[![j-dohnalek](https://avatars.githubusercontent.com/u/6439903?v=4)](https://github.com/j-dohnalek "j-dohnalek (1 commits)")[![laravel-shift](https://avatars.githubusercontent.com/u/15991828?v=4)](https://github.com/laravel-shift "laravel-shift (1 commits)")

---

Tags

ahmedashraf093better-eloquent-state-machine

###  Code Quality

TestsPHPUnit

### Embed Badge

![Health badge](/badges/ahmedashraf093-better-eloquent-state-machine/health.svg)

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

###  Alternatives

[fumeapp/modeltyper

Generate TypeScript interfaces from Laravel Models

196277.9k](/packages/fumeapp-modeltyper)[aedart/athenaeum

Athenaeum is a mono repository; a collection of various PHP packages

245.2k](/packages/aedart-athenaeum)

PHPackages © 2026

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