Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Laravel State Machine Laravel Package

iotron/laravel-state-machine

A Laravel package for building state machines around Eloquent models. Define states and allowed transitions, guard status changes, and keep workflow logic organized, explicit, and easy to test. Helps replace scattered status checks with a clear, maintainable transition map.

View on GitHub
Deep Wiki
Context7

Getting Started

This package is a drop-in replacement for asantibanez/laravel-eloquent-state-machines, so the mental model is the same: attach a state machine to an Eloquent model, declare allowed transitions, and call transitionTo() from your application code.

Minimum setup:

composer require <vendor>/<package>

Requirements as of v1.0.1: Laravel 11.x, 12.x, or 13.x; PHP 8.2–8.4. Note that Laravel 13 requires PHP 8.3+, so the PHP 8.2 + Laravel 13 combination is intentionally unsupported.

Publish any package config and the state-history migration if you want an audit trail, then run migrations. If you are migrating from asantibanez/laravel-eloquent-state-machines, verify the composer.json replacement/alias and keep your existing state machine classes where possible.

First use case — an order lifecycle:

use Asantibanez\LaravelEloquentStateMachines\Traits\HasStateMachines;

class Order extends Model
{
    use HasStateMachines;

    public $stateMachines = [
        'status' => OrderStatusStateMachine::class,
    ];
}
use Asantibanez\LaravelEloquentStateMachines\StateMachines\StateMachine;

class OrderStatusStateMachine extends StateMachine
{
    public function transitions(): array
    {
        return [
            'pending'  => ['approved', 'rejected'],
            'approved' => ['shipped'],
            'rejected' => [],
            'shipped'  => [],
        ];
    }

    public function defaultState(): string
    {
        return 'pending';
    }
}

Then:

$order->status()->canTransitionTo('approved'); // guard first
$order->status()->transitionTo('approved');
$order->status()->is('approved');

Where to look first: the model trait and $stateMachines map, the state machine’s transitions() and defaultState(), then the history API if you need auditing.

Implementation Patterns

Keep transition definitions declarative. Put the allowed graph in one state machine class per concern. Avoid scattering if ($order->status === ...) checks across controllers, jobs, and listeners; use canTransitionTo() as the single guard.

Wrap multi-step work in an application transaction. This is the main v1.0.1 improvement. The state machine now checks DB::transactionLevel() before opening a transaction. If your service already wraps the transition in DB::transaction(), the state machine reuses that boundary instead of creating a nested savepoint:

DB::transaction(function () use ($order) {
    $order->status()->transitionTo('approved');
    $order->approved_at = now();
    $order->save();

    Inventory::reserve($order);
});

This is the pattern to prefer for anything that changes more than the state field. It keeps the transition, model updates, and related writes atomic.

Use hooks or custom State classes for side effects. Rather than firing notifications directly after transitionTo(), place side effects in the package’s transition hooks or custom state classes when they are intrinsically tied to entering/leaving a state. Keep external I/O (mail, HTTP, queue dispatch) out of the immediate transaction path or dispatch with afterCommit():

SendOrderApprovedNotification::dispatch($order)->afterCommit();

Use history for audit and debugging. If history tracking is enabled, history() gives you the transition trail. This is useful for support tooling and for asserting behavior in tests. Index the history table on the morph columns and timestamp if you expect volume.

Use enums where they fit. v1.0.1 improved enum support and tightened State typing. Backed enums work well for state names, but keep transition keys and enum values aligned exactly. Add tests around enum-backed states if you rely on them.

Test transitions, guards, and transaction behavior. Cover:

  • allowed and disallowed transitions,
  • default state on creation,
  • history records,
  • side effects dispatched after commit,
  • behavior inside and outside an existing transaction.

The new TransactionBoundaryTest in the package is a good reference for the last point.

Gotchas and Tips

Transaction behavior changed in v1.0.1. Previously, a transition inside an existing transaction could create a nested savepoint. Now it reuses the current transaction level. The practical consequence: if the outer transaction rolls back, the transition rolls back too — usually what you want, but different from savepoint-based isolation. If you explicitly need partial rollback semantics, wrap the transition in your own DB::transaction() call so Laravel creates the savepoint you asked for. Do not assume the state machine creates one for you anymore.

Concurrency needs explicit locking. A guard check followed by a transition is still a read-then-write race. For order/payment/inventory flows, lock the row inside the transaction:

DB::transaction(function () use ($orderId) {
    $order = Order::lockForUpdate()->findOrFail($orderId);
    $order->status()->transitionTo('shipped');
});

Laravel 13 + PHP 8.2 is not supported. The release notes call this out explicitly. If your CI matrix includes that combination, remove it or pin Laravel 13 to PHP 8.3+.

Watch test transaction strategy. RefreshDatabase and DatabaseTransactions put tests inside a transaction. Since the state machine now inspects DB::transactionLevel(), tests that assert rollback or savepoint behavior must account for the test’s own transaction. Use explicit transaction tests where boundary behavior matters.

Migration/config publishing is easy to miss. History tracking will fail or silently not record if the migration or config is absent. Check the published config for the history model/table and confirm migrations ran in every environment.

Custom State subclasses deserve a regression run. v1.0.1 tightened State typing and corrected Collection imports. If you extended State or overrode methods returning collections, run your state machine tests after upgrading.

Stale model state after transitions. Refresh the model if you continue using the same instance after a transition, especially when hooks or observers modify attributes. For API responses, reload relationships and state explicitly.

Events and jobs should generally be afterCommit. A transition inside a transaction that dispatches a queued job immediately can leak work before commit. Use afterCommit() for notifications and downstream integrations.

Drop-in replacement caveat. Because this package mirrors asantibanez/laravel-eloquent-state-machines, most imports and APIs should carry over. Still verify namespace imports, config keys, and history table names in a branch before swapping it into a production application.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
robertmarney/lara-hierarchial-collections
simonschaufi/rule-doc-generator
emissaoja/emissaoja
blackbricksoftware/gravityforms-civicrm-form-processor
al-rahabi/fancy-crud
jinas/dhivatar-component
mltstephane/laravel-quality
bernskiold/laravel-talentlms
bernskioldmedia/laravel-form-architect