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 Interlock Laravel Package

robertboes/laravel-interlock

Laravel Interlock is a lightweight package for preventing concurrent execution of critical code in Laravel apps. It helps avoid race conditions, duplicate jobs, and overlapping tasks with simple, configurable locks and a clean framework-friendly API.

View on GitHub
Deep Wiki
Context7

Getting Started

Interlock adds guarded state machines to Eloquent models: a state attribute becomes read-only from the outside, and only declared transitions may change it. That single rule is the whole package — everything else (history, triggers, exports) hangs off it.

Setup:

  1. composer require robertboes/laravel-interlock and publish the config (php artisan vendor:publish — take the interlock tag). If you want the audit trail, publish and run the history migration too; it is opt-in.
  2. If you use Laravel Boost, the package ships guidelines, so re-run your Boost install/sync and your agent will already know the API surface. That is the fastest way to get correct transition scaffolding.
  3. Read the config file first. It names the history recorder, the trigger command/job routing, and the discovery registry cache — those three settings explain most behaviour you'll observe.
  4. Find where a model opts into discovery (marker trait/interface/attribute — the discovery docs list the exact hook, and the registry is cached, so clear it when you add models).

First use case: take one model with a status column (Order, Invoice, Subscription). Declare its machine with the states you actually have, mark the terminal ones as final, then replace every $order->status = 'paid'; $order->save(); with a transition call. The moment you do, any remaining direct write will throw — which is the point, and also the thing that will break your seeders, factories and older controllers.

Validate your mental model before writing much code: run the interlock:signalbox export and look at the Mermaid diagram. If the picture is wrong, the machine is wrong.

Implementation Patterns

Transitions as classes, not closures. Each transition is a class with apply and after-commit steps. Put the domain mutation in apply (update related rows, adjust totals) and everything with a side effect — mail, webhooks, broadcasts — in the after-commit step so it cannot fire on a rolled-back transaction. Business rules go in guards; actor permissions go in authorize($actor).

Drive UI from available transitions. Ask the package which transitions a given actor can apply to a model right now, and render buttons from that list. Each entry tells you which state attribute it belongs to, which matters once a model has more than one machine. Do not hand-maintain a list of "allowed statuses" in Blade — it drifts from the guards.

Staged transitions for clean writes. Stage a transition and let the next save() emit it in a single UPDATE. This is the pattern to prefer over mutating state and saving twice, and it composes with afterCommit nicely.

Handle staleness explicitly. Transitions use compare-and-swap and throw a stale-state exception when the row moved underneath you. Catch it in the controller and return 409 / re-render, or retry in a job. Do not catch-and-ignore; that is how you get double-shipped orders.

System scope for infrastructure transitions. Jobs, console commands and webhooks that are not acting as a user should run inside the system scope so system-only transitions apply. Keep that scope as tight as the bypass (below) — it is an escape hatch with a nicer name.

Multiple machines per model. Split status and payment_status into separate machines rather than one giant graph. Constraint to remember: transition names must be unique across all machines on the model, so prefix them (markPaid, cancelOrder) instead of reusing cancel.

Query with scopes. Use the state scopes for dashboards and reports (pending, overdue) rather than raw where('status', ...). Opt-in scope methods keep model noise down until you actually need them.

Time in state. Entry timestamps are recorded, so "in pending for more than 24h" is a query, not a cron with a custom column. This is usually the cheapest win after the first transition — SLA dashboards and escalation jobs become one-liners.

Triggers for autonomous behaviour. A trigger applies its transition once a condition (any callable) holds, without checking authorization — so the condition is the security boundary; keep it narrow and server-derived. Configure queue connection/queue per trigger, cap runs, and let busy triggers fan out into chunk jobs. Add the status command to your monitoring and treat "overdue trigger" as a page, not a log line.

Bypass, deliberately. Factories, seeders and opted-in tests write state directly inside a reasoned bypass. Because bypassed writes are recorded in history, prefer a bypass over dropping enforcement in test config — the intent stays visible in the audit trail.

Test at the transition boundary. Fake Interlock for unit-ish tests, and assert on transitions (applied / denied by guard / denied by authorization) in feature tests. Set up fixtures via factory bypass rather than walking the model through ten legal transitions.

Export into design review. php artisan interlock:signalbox gives Mermaid, XState JSON/TypeScript, and a --viz Stately Sketch export. Paste the Mermaid into the PR or ADR; ship the XState TS to the frontend team so their optimistic UI mirrors the real graph. Generate in CI, commit the artefact, diff it.

Soundness check in CI. Declare final states, then let the soundness check run in CI. It catches unreachable states, dead ends and missing terminal states before they become "why is this order stuck in processing" tickets.

Gotchas and Tips

  • v0.1.0 means the API can move. Pin the version, and read the changelog on every bump. Expect naming churn in config keys and command options before 1.0.
  • Everything writes through transitions now. Direct attribute assignment, update([...]), the query builder's mass update, insert, factories and replicate all interact with enforcement (replication deliberately resets state). Grep your codebase for ->update([ and ->status = on interlocked models before you ship the upgrade — this is the whole migration.
  • Bypass is a scalpel. Wrap only the write, always give a reason (it is recorded), and never leave a bypass on a production request path. If you find yourself adding one to a controller, you are missing a transition.
  • Missing authorization = denied. A transition that declares no authorization is rejected. That's a safe default and a confusing first error: internal transitions still need an explicit authorize or must run in system scope / as a trigger.
  • Triggers skip authorization entirely. Combine that with a callable condition and you have a "run arbitrary transition when this closure says so" primitive. Never let the condition depend on request input.
  • Stale-state exceptions under load. Two workers transitioning the same row is normal traffic, not an error condition. Decide per call site: retry, 409, or ->refresh() and re-evaluate available transitions.
  • After-commit steps can run more than once in failure/retry scenarios. Make webhooks and mail idempotent, or you will ship duplicate emails during a queue backlog.
  • Discovery is cached. A newly created machine/model silently doesn't exist until the registry cache is cleared. Wire the clear into your deploy script and note it in the README for new devs.
  • Unique transition names across machines on the same model. Collisions surface at boot/registration, but the error message points at the machine, not the colliding one — check all machines on that model.
  • History is opt-in and forward-only. Turning it on later leaves gaps and null entry timestamps; time-in-state queries return nothing for rows created before the migration. The table is shared across models with integer/UUID/ULID keys — check key types before adding a model with a different primary key strategy.
  • Changing the history recorder in config affects new writes only; old rows stay attributed to the previous recorder.
  • Triggers on sync in tests fire inline and can loop. Use the cap and chunk fan-out to bound this, and always drive trigger tests with a fake or a Bus::fake().
  • Nested states need a semantics check. Confirm whether a parent-state scope matches children before building a dashboard on it — "is shipped a processing state?" is a question your reporting code will ask.
  • Exports are artefacts, not runtime. The XState TS is for frontends and designers; regenerate rather than hand-editing, and don't let the exported file become the source of truth.
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.
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
youbar/easy-crud
c-point-labs/laravel-hostyourai
dbravoan/dbcms-admin-theme-dbcms
dbravoan/dbcms-theme-db-cyber
dbravoan/dbcms-blocks-basic
dbravoan/dbcms-module-content-audit
dbravoan/dbcms-module-ai-assistant
paygate-to/paygate-php
padosoft/askmydocs-connector-base
defstudio/supercharged-carbon
bernskioldmedia/laravel-matomo