laravel-model-flags
Add lightweight, queryable boolean flags to Eloquent models without adding a migration column for every piece of state you need to track.
Repository Health
Technical Analysis
Laravel Model Flags is a small Spatie package that lets any Eloquent model carry named boolean flags without a schema change. A single HasFlags trait wires the model up to a polymorphic flags table, so you can mark a record as flag()’d, check it with hasFlag(), and remove it with unflag() — all backed by a real, queryable relation instead of a JSON blob or a growing set of nullable columns.
The primary use case is idempotency: making long-running or resumable operations safe to re-run. A common example is an Artisan command that emails every user — by flagging each user after a successful send and scoping the next run to notFlagged(), a crashed or cancelled job can be restarted without re-sending mail to anyone who already received it. The same mechanism works for tracking migration/backfill progress, one-time onboarding steps, or any per-model “has this happened yet” state.
Beyond the basic flag/unflag pair, the package exposes flagged()/notFlagged() query scopes for filtering models by flag state, flagNames() to list all flags on a model, and latestFlag()/lastFlaggedAt() to inspect when a flag was last touched (re-flagging updates the timestamp rather than duplicating the row). Flags accept plain strings or PHP backed enums, and are automatically cleaned up via a deleted model event when the owning model is removed.
What You Get
- A
HasFlagstrait that addsflag(),unflag(), andhasFlag()methods to any Eloquent model flagged()andnotFlagged()query scopes for filtering models by flag presenceflagNames(),latestFlag(), andlastFlaggedAt()for inspecting a model’s current and historical flag state- Support for both plain strings and PHP backed enums as flag names
- A published migration and config file (via
spatie/laravel-package-tools) so theflagstable and flag model are customizable - Automatic flag cleanup on model deletion via a bootable model event
Common Use Cases
- Making an Artisan command that emails or processes every user resumable after a crash, by flagging each record once handled
- Tracking one-time onboarding or setup steps per user without adding a boolean column per step
- Recording backfill or data-migration progress per row so a long-running script can be safely restarted
- Marking records for later review or follow-up (e.g. “needs manual check”) without a dedicated status column
- Auditing when a particular event last happened on a model via
lastFlaggedAt()
Under The Hood
Architecture
The package is intentionally small: a single HasFlags trait mixed into consumer models, a Flag Eloquent model with a morphTo flaggable relation, and a ModelFlagsServiceProvider that registers the config and migration through Spatie’s shared laravel-package-tools base class. The trait itself is the whole feature surface — flags() defines the morphMany relation (model class configurable via config('model-flags.flag_model')), and flag()/unflag()/hasFlag() are thin wrappers around firstOrCreate/whereIn/exists calls against that relation. A bootHasFlags() hook registers a deleted model event to cascade-delete flags when the owning model is removed, so there’s no orphaned-row cleanup job needed. Because everything hangs off Eloquent’s own relation and boot-trait mechanisms, the package adds no runtime services, queues, or background processes — it changes behavior only through the trait a consumer opts into.
Tech Stack
Pure PHP 8.1+ targeting Laravel’s illuminate/contracts across a wide compatibility range (^9.0 through ^13.0), so it works with several concurrent Laravel LTS/current releases. It depends on spatie/laravel-package-tools for service-provider boilerplate (config/migration publishing) rather than hand-rolling that logic. Dev tooling is Pest (versions 1 through 4, again spanning multiple majors) with the pest-plugin-laravel extension, Orchestra Testbench for an isolated Laravel test app, spatie/test-time for freezing time in timestamp-sensitive tests, and Laravel Pint for style enforcement — all fairly standard for a Spatie-maintained Laravel package.
Code Quality
Tests are thorough for the package’s scope: HasFlagsTest.php and FlagTest.php cover flagging, unflagging (single and array forms), the flagged/notFlagged scopes, flag-name listing, latest-flag lookup, timestamp updates on re-flagging, and cascade deletion on model removal — using Pest datasets to parameterize over string and enum flag names. TestCase boots a real Testbench Laravel app with an in-memory test_models table so relations are exercised against a live Eloquent stack rather than mocks. CI runs the test suite via GitHub Actions (run-tests.yml) and auto-fixes style issues on push via a Pint-based workflow, plus a changelog-update workflow — a lightweight but complete quality gate for a package this size. There’s no static analysis (PHPStan/Psalm) configured.
What Makes It Unique
The package doesn’t invent a new concept — Laravel already has patterns for tagging and status columns — but it deliberately optimizes the flag/unflag/hasFlag API around the idempotent-batch-job use case: a notFlagged() scope paired with a per-record flag() call is a very small amount of code to make an otherwise all-or-nothing bulk operation safely resumable. Supporting backed enums alongside plain strings, and tracking per-flag updated_at timestamps (rather than just existence) so lastFlaggedAt() works, are the two specific design choices that differentiate it from simply adding a boolean column.