exolnet/laravel-status
Exolnet Laravel Status adds flexible, database-backed status management to Laravel Eloquent models. Define available statuses, assign them to records, and track changes through a simple trait, making it easy to model workflows and record state across your application.
exolnet/laravel-status gives Eloquent models a first-class status field plus the plumbing around it (allowed values, transitions, scopes, and typically a history/audit trail). It is the kind of package you reach for when a status string column starts accumulating if statements in controllers.
Version floor first. As of v2.14.0 the package no longer supports Laravel 11 — you need Laravel 12 (and the PHP version that implies). Before anything else, confirm composer.json:
composer require exolnet/laravel-status:^2.14
composer why-not exolnet/laravel-status ^2.14 # if it refuses to resolve
If you are still on Laravel 11, pin ^2.13 and plan the framework upgrade separately — do not try to mix.
Then:
php artisan vendor:publish --provider="Exolnet\Status\StatusServiceProvider"
php artisan migrate
First use case: an Article with draft → review → published → archived. Wire the trait, add the cast, then replace a $article->status = 'published'; $article->save(); with a guarded transition. That single swap is where the package starts paying for itself.
Cast the status, don't compare strings. Whether you use a PHP enum or the package's status class, cast it on the model so $article->status is always a typed object. String comparisons scattered across views and policies are the main source of "why is this row invisible" bugs.
Express transitions as data, not controller logic. Define which statuses may move to which, then funnel writes through a single method/service. Controllers, Filament actions, and Artisan commands should all call the same transition entry point so guards and history stay consistent.
Use scopes for list screens. Article::whereStatus(Status::Published) (or the package scope) keeps filters composable and index-friendly. Combine with withCount/with for admin tables rather than loading statuses per row.
History as an audit trail, not a UI feature. The status history table is your forensic record. Treat it as append-only; never backfill or delete from it in application code. When someone asks "who unpublished this at 3am", that table is the answer.
Gate side effects on the transition, not the save. Notifications, search-index updates, and webhooks belong in an observer or a dedicated listener fired on status change — not in booted() model hooks that also fire on unrelated saves. This keeps bulk operations from spamming Slack.
Test the transitions, not the trait. Your test suite should assert your domain rules (draft cannot jump to archived), not re-verify the package's own behaviour. Use factories with named states (->published()) so fixtures read as intent.
Integration points worth knowing: authorization policies keyed on status (canPublish), scheduled commands that expire content (whereStatus(...)->where('published_at', '<', now())), and API resources that expose a status string plus a can_transition_to map so the frontend doesn't hardcode the state machine.
The Laravel 11 drop is the headline. v2.14.0 removes Laravel 11 support and bumps dependencies. Consequences to check before upgrading:
composer.lock must move to Laravel 12 first or resolution will fail confusingly.illuminate/support ^11 will block the upgrade even though it has nothing to do with statuses. Run composer why-not laravel/framework ^12 to find the real culprit.composer.json diff of the release, not just the changelog. "Update dependencies" often hides a PHP minor-version bump or a major in a support library.Refresh after transition. Status guards frequently read the in-memory model. After a transition that writes through a query builder or a service, call $model->refresh() before branching on $model->status, or you will act on a stale value.
Mass assignment. Keep the status column out of $fillable unless you genuinely want it settable from request payloads. Status should change through transitions, not Article::create($request->all()).
Migration publishing collisions. If you published the migrations under an older version and later publish again, you can end up with two migration files touching the same table. Check database/migrations for duplicates and diff against the package stub before running in production.
Config caching. Changes to the published config require php artisan config:clear in local dev and a cache rebuild on deploy. Status definitions read from config at boot are a classic "works locally, wrong in staging" trap.
N+1 on history. Rendering a timeline for a list of models will fire one query per row. Eager-load the history relation, or paginate the timeline separately with a dedicated endpoint.
Extension points. When the built-in transitions aren't enough, prefer subclassing the status class or registering a custom guard over editing vendor files — and keep a note of which methods you override, because a minor release can change the surrounding flow.
Debugging. Enable query logging and look at the actual UPDATE plus the history insert. Most "the status didn't change" reports are a failed guard swallowing an exception, or a transaction rolled back around the side effect rather than the status write.
How can I help you explore Laravel packages today?