caner/state-machine
A lightweight Laravel state machine for Eloquent models. Define states, transitions and guards, apply them safely with validation, events and history, and keep workflow logic out of controllers and models. Ideal for approvals, orders and multi-step processes.
Architecture fit. Unchanged. The package still addresses a well-understood domain pattern — finite state machines over Eloquent models — that maps cleanly onto workflow-driven domains (order lifecycle, payments, subscriptions, onboarding/KYC, ticketing, moderation). It remains a thin, composable library rather than a framework. The 1.1.0 "automatic state update" feature is a meaningful architectural addition, not cosmetic: it implies the package will persist state changes itself rather than requiring an explicit save(). That is convenient, but it moves the package from a passive definition layer into the write path, which is exactly where transaction boundaries, model events/observers, and enum casts collide. This needs to be understood before the architecture-fit judgement can stand.
Integration feasibility. Materially worse than the prior assessment implied. Release 1.1.0 advertises "Laravel 9 compatibility added," which strongly suggests the package's supported ceiling is Laravel 9 — below our Laravel 10/11 target stack. The prior assessment treated current-Laravel compatibility as an open question to verify; this release answers it in the unfavourable direction. Combined with the still-thin maturity signals (single maintainer, low star count, no dependents), the practical integration path is now most likely "vendor/fork and backport" rather than "composer require." The added example project and doc updates are a genuine improvement to evaluability — we can now read working code rather than infer intent — but they do not change the compatibility picture.
Technical risk. The dominant risk is unchanged and now sharper: abandonment/bus factor compounded by a version ceiling behind our stack. A second, newly introduced risk is behavioural: "automatic state update" is underspecified in the release notes. We do not know whether it writes inside or outside the caller's transaction, whether it fires saving/saved/observer hooks, whether it respects or bypasses lockForUpdate, whether it performs a full save() (risking clobbering concurrent column updates) or a targeted update, and whether it can be disabled. A convenience feature on the write path in a low-maintenance package is precisely where silent last-write-wins bugs live. Correctness risk around "transaction-safe" remains unvalidated. Security exposure remains low (no network I/O, no credential handling). The prior composite score of 0.315 should be held at best and is more likely to move downward pending compatibility verification.
Key questions.
composer.json constrain Laravel to ^9.0 or below? Is there any path to Laravel 10/11, or is a fork required?save(), targeted update(), or attribute-only? Is it opt-in or default-on?spatie/laravel-model-states, if both are present?Stack fit. Unchanged in principle: a natural fit where an entity has an explicit lifecycle column and discrete, auditable transitions. The new caveat is that "automatic state update" should be treated as a second, opt-in abstraction layered on the core machine. If we adopt, decide deliberately whether automatic persistence is on or off — default to off until its transaction and event semantics are proven, and keep explicit save() inside our own transaction boundary.
Migration path. The POC now has a mandatory pre-step: run the package against our actual Laravel version in a throwaway app and record exactly what breaks. If it does not install or run, the POC proceeds on a vendored fork with the framework constraints widened and any Laravel 10/11 breaking changes patched. The POC itself should cover both modes — automatic update on and off — and must include a concurrency test (two processes transitioning the same row) plus an observer/event-ordering test, since that is where the new feature is most likely to surprise us.
Compatibility. This is now the gating item rather than a checkbox. Build the compatibility matrix immediately; the expected outcome is "Laravel 9 only," which forces the fork path for a 10/11 codebase. Pin with composer.lock discipline, and if we fork, treat the fork as the supported artifact and track upstream as an input rather than a dependency.
Sequencing. (1) Compatibility spike against Laravel 10/11 — fast, decisive, do this first. (2) Source review focused on the new automatic-update path and lock acquisition. (3) Fork/backport if required, with our own CI. (4) POC on a low-risk model covering automatic-update on/off, concurrency, and observer ordering. (5) Pilot on one transactional domain with load testing. (6) Broader rollout only if prior gates pass. The fork decision has moved from a late checkpoint (step 4 previously) to a near-certain early step.
Maintenance. Assume unmaintained, with a supported-version ceiling behind our stack. Fork, document, and assign a maintainer before any production use — this is now a precondition rather than a recommendation. Ongoing effort remains modest (small codebase) but is higher than previously estimated, because a fork must carry forward framework-compatibility patches and absorb the new automatic-update code path into our review surface. Effort still spikes at each Laravel major upgrade, and we now know we are already carrying one such gap.
Support. Community-only, no SLA, unchanged. The updated documentation and linked example project measurably improve troubleshooting — there is now a reference implementation to compare against — but the owning team should still expect to read source for anything involving the write path or concurrency.
Scaling. Unchanged core profile: state machines are computationally cheap; contention concentrates in row-level locks on hot entities. One new consideration — if "automatic state update" performs a full model save or a write outside the caller's lock, it can widen the write footprint and increase lost-update surface under concurrency. Keep transactions short, keep external calls (HTTP, email, queue dispatch) out of transitions, and use post-commit hooks for side effects.
Failure modes. Prior list stands: illegal-transition exceptions surfacing to users, stuck states requiring manual remediation, lock timeouts and deadlocks, partial side effects before commit, event/observer ordering surprises, misconfigured transitions acting as silent no-ops, schema drift between state column and machine definition. Add to it: unintended automatic persistence (state written at a point we did not intend), double writes or redundant saved events, lost updates if automatic update bypasses the lock, and divergent behaviour between automatic and manual transition paths if both are used in the codebase. Each needs a runbook entry and alerting, and the automatic-update cases need explicit test coverage before rollout.
Ramp-up. Slightly improved. The doc refresh and example project reduce the "read the source to understand the API" tax; estimate roughly one day for a senior engineer to become productive, plus a short team session on guards, transitions, transaction boundaries, and — newly — on when automatic state update is permitted versus explicit persistence. If we fork, add onboarding time for the fork's maintenance conventions and release process.
How can I help you explore Laravel packages today?