sebdesign/laravel-state-machine
A lightweight Laravel package for adding state machines to Eloquent models. Define states, transitions, and guards, then move records safely between states with validation, events, and clear exceptions. Integrates cleanly with Laravel and supports custom classes.
Architecture fit
sebdesign/laravel-state-machine is a thin Laravel integration layer over Symfony's symfony/workflow component. It does not implement its own state engine; it contributes a service provider, configuration-driven graph/state-machine definitions, a registry/factory for resolving machines, and a HasStateMachine-style trait for Eloquent models. That is a favourable architecture for us: business rules for lifecycle transitions live in config and guard/transition classes rather than being smeared across controllers, and the underlying engine is a mature, widely deployed Symfony component. It fits bounded-context domains with explicit lifecycles (order fulfilment, subscription billing, approval/onboarding, document review, KYC) far better than ad-hoc status enums plus scattered if checks, and it is a lighter commitment than introducing an event-sourcing framework.
The trade-off is that the package's value is concentrated in glue code, not in the engine. Its longevity therefore tracks two independent variables: Symfony Workflow's API stability and Laravel's service-provider/config conventions. Both are stable, so the architectural risk is moderate-low.
Integration feasibility
Integration is inexpensive in engineering terms: composer require, publish the config, declare states/transitions, apply the trait to the relevant models, and optionally listen for transition events. A pilot on a single entity is realistically a one-to-two sprint side-work item, most of which is domain modelling rather than plumbing. The release under review (v3.4.7) is a single-change patch: a Laravel 13.x compatibility contribution from laravel-shift. For any team planning to move onto Laravel 13, this is the gating item — prior to it, the package's illuminate/* constraints would have blocked the framework upgrade or forced a --ignore-platform-reqs workaround in CI. Nothing in the notes indicates breaking API change, config change, or deprecation.
Technical risk
composer.json. Untested constraint widening is the classic source of "works until it doesn't" incidents.Key questions for the maintainer / our own verification
symfony/workflow range?Stack fit Assumes a conventional Laravel application (Eloquent models, service container, config publishing, queued event listeners). It composes cleanly with our existing patterns — policy/authorisation checks map naturally onto guards, and transition events map onto our existing domain-event and audit-logging conventions. It is a poor fit only where we already have a workflow engine (e.g. Temporal/Camunda) in the same bounded context; we should not run two orchestrators over one lifecycle.
Migration path
Compatibility v3.4.7 is the minimum viable version for any Laravel 13 workstream. Teams remaining on Laravel 11/12 can stay on 3.4.6 without loss of function, since the only delta is framework compatibility. Conversely, the Laravel 13 upgrade is now unblocked at the dependency level — but should not be treated as validated until we have run our own suite against 13 with this version pinned.
Sequencing
^3.4.7 in composer.json; run the full test suite plus a config-cache warm on the target Laravel version.Maintenance Low code-maintenance burden; the cost is configuration ownership. Every new state or transition is a config change that needs review from whoever owns the domain rules — this shifts review load from engineers to domain owners, which is desirable but must be resourced. Budget a recurring dependency-review slot per framework or Symfony major.
Support
No vendor support; reliance on upstream OSS and internal expertise. Recommend designating one owner (a "champion") per adopting team, plus a documented fallback: because the engine is Symfony's, an exit path to using symfony/workflow directly, or to a hand-rolled transition table, is realistic and cheap. That materially de-risks the dependency.
Scaling Runtime overhead is negligible — transition resolution is in-memory config evaluation. Scaling concerns are data-side: state column indexing, transition-history table growth (if we persist it), and write contention on hot aggregates. Config caching keeps per-request definition parsing out of the hot path; verify the package's config is cache-friendly on 13. Operationally, the number of declared graphs becomes a cognitive-load problem before it becomes a performance problem — enforce naming conventions and co-locate each graph with its owning domain.
Failure modes to instrument
Ramp-up A developer with Laravel experience needs roughly a day to be productive with the trait and config; genuine fluency — guards, transition events, history modelling, and the concurrency story — is closer to one to two weeks of paired work on a real lifecycle. The dominant learning cost is not the package API but the discipline of modelling state explicitly. Plan a short internal walkthrough before the second team adopts it, and capture the pilot's conventions as the reusable template; that is the cheapest way to prevent each team from inventing its own dialect of states and transition names.
How can I help you explore Laravel packages today?