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

State Machine Laravel Package

hyn/state-machine

A lightweight state machine for Laravel that lets you define states and transitions on Eloquent models, validate and apply transitions cleanly, and keep business logic out of your controllers.

View on GitHub
Deep Wiki
Context7

Getting Started

  1. Install and wire it up

    composer require hyn/state-machine
    

    On Laravel 5.5+ the service provider is auto-discovered; on older versions add Hyn\StateMachine\StateMachineServiceProvider::class to config/app.php. Publish any config with php artisan vendor:publish --provider="Hyn\StateMachine\StateMachineServiceProvider" and read the generated file first — it is the authoritative map of how machines, states, and transitions are declared in this version.

  2. Look at these three things before writing code

    • The published config / README example showing a states + transitions definition.
    • The trait you put on your model (Hyn\StateMachine\Traits\StateMachineTrait) — its public methods are your day-to-day API (transition, canTransition, state getters).
    • The package's event class fired on transition — subscribe to this for all side effects. Names are stable enough to grep for Events/ in vendor/hyn/state-machine/src.
  3. Attach it to one model, with one column

    Add a state string column (nullable or defaulted) to the table, then:

    class Order extends Model implements \Hyn\StateMachine\Contracts\StateMachineContract
    {
        use \Hyn\StateMachine\Traits\StateMachineTrait;
    
        // states/transitions definition here, per package docs
    }
    

    Keep the column a plain string — do not use a DB enum, it fights every migration you'll ever write.

  4. First use case

    Model an order lifecycle: pending → paid → shipped → completed, with cancelled/refunded as terminal branches. Drive it from a controller or action:

    $order->transition('pay');
    

    Guard it with $order->canTransition('pay') before rendering the button. This is the 80% use case: a handful of states, a handful of named transitions, and one place to hook events.

Implementation Patterns

  • Wrap transitions in domain actions, not controllers. Create App\Actions\PayOrder that loads the order, checks authorization and business rules, then calls transition('pay'). Controllers stay thin, and you have one seam to add locking, logging, or retries later.

  • One listener per side effect. Register listeners on the package's transition event to send mail, write audit rows, push to a queue, or update a search index. Pass only IDs into queued listeners and re-fetch the model — the serialized Eloquent model will be stale by the time the worker runs.

  • Expose available transitions to the UI/API. Render buttons from canTransition(...) checks rather than hard-coding if ($order->state === 'pending') in Blade. In API resources, return an available_transitions array so the frontend never needs to know the graph.

  • Guard with authorization, then state. Use a policy for "can this user pay this order?" and the state machine for "is this order payable at all?". Combine them; don't overload either.

  • Test transitions directly. Use RefreshDatabase, put the model in a known state via factory, call the transition, assert the new state. For listeners, Event::fake() in one test, and a separate integration test asserting listeners actually run.

  • Use it for multi-step approval flows too, not just commerce. draft → review → approved → published on content, invited → active → suspended on users. The pattern is the same; the value is centralizing the graph so it can't drift between controller, job, and admin panel.

  • Import/bootstrap carefully. When seeding or migrating existing rows, either skip the machine and write state directly, or add a transition like initialize that is allowed from null. Never let a data migration throw InvalidTransition.

Gotchas and Tips

  • Invalid transitions throw. A transition not defined from the current state raises an exception. Catch it at the controller boundary and return 422/409 rather than a 500. Call canTransition() before transition() in user-triggered paths.

  • Events and database transactions. If you dispatch inside DB::transaction() and the transaction rolls back, listeners that already ran (mail, webhooks) will not be undone. Use DB::afterCommit() (or the after_commit queue option) for anything with an external effect.

  • Concurrency. Two requests can read the same state and both attempt a transition. Wrap the action in DB::transaction() with lockForUpdate() on the row, or accept last-write-wins consciously.

  • Idempotency. A retried job or double-clicked button may transition twice or throw. Make actions idempotent by checking current state first and treating "already in target state" as success.

  • This is a small, low-activity package (16 stars). Read the source before depending on it: it is small enough that you can vendor it mentally in an hour. Pin the version in composer.json, and put your own action classes in front so swapping to symfony/workflow or a hand-rolled switch later is a contained change.

  • Config caching and definition location. If states/transitions live in config, remember php artisan config:clear in dev after edits. If they live on the model, they don't cache — but they also can't be changed at runtime.

  • Extension points. The graph definition and the event listener are the two places to customize. Avoid subclassing the package's internal transition/state classes unless you must; override behavior in your actions and listeners instead.

  • Don't let the state machine do validation. It answers "is this transition legal from here?", not "is this order total correct?". Keep business-rule validation in form requests or the action.

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.
shitoudev/permissionmanager
johannesclimacus/laravel-tui
hocvt/seotools
vitalybaev/sypexgeo
amphibee/themosis-ignition
gito-me/fast-excel
frankyso/laravel-ipaymu
geolize-studio/laravel-chile
boomerangui/chat
jinas/dhivatar
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console