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 Model Flags Laravel Package

spatie/laravel-model-flags

A Laravel trait that adds flags to Eloquent models without migrations, plus scopes to query flagged models. Useful for tracking process state and building idempotent, restartable code like Artisan commands.

View on GitHub
Deep Wiki
Context7

Technical Evaluation

Architecture fit spatie/laravel-model-flags adds a single polymorphic flags table plus a HasFlags trait that exposes flag(), unflag(), hasFlag(), flagNames(), lastFlaggedAt(), and the flagged() / notFlagged() query scopes. That is a narrow, well-understood persistence primitive, and it fits any Eloquent-based codebase without touching existing schema. The best-fit use cases are exactly the ones the README names: idempotent/restartable artisan commands, one-time backfills, per-record "this happened" markers, and lightweight progress checkpoints on jobs. It is a poor fit for anything state-machine-like (no transition rules, no ordering guarantees), for high-cardinality event data (one row per flag per model with no retention policy), and as a substitute for job-level idempotency keys or database advisory locks in concurrent-worker scenarios. Treat it as a convenience layer over a has-many relation, not as workflow infrastructure.

Integration feasibility Low effort. composer require, publish + run one migration, add one trait to the target models. The published config exposes only flag_model, so extending Spatie\ModelFlags\Models\Flag for custom behaviour (retention, casts, observers) is the supported extension point. Changelog shows an unbroken compatibility ladder — Laravel 11 (1.3.0), 12 (1.4.1), 13 (1.4.2) — and PHP 8.0+ support since 1.0.1. Realistic integration estimate for a mid-level Laravel developer: half a day to wire up and smoke-test, 1–2 days including backfill and test coverage.

Technical risk The dominant risk is concurrency semantics. The documented idempotency pattern is check-then-act (notFlagged(...)->each(... flag())), which is safe for a single sequential command and unsafe the moment two workers process the same model concurrently — both can read "not flagged" before either writes. The README does not document a unique constraint on (flaggable_type, flaggable_id, name), so concurrent flag() calls on the same name may produce duplicate rows or rely on an undocumented upsert; verify the shipped migration before depending on it. Secondary risks: hasFlag() appears to hit the flags relation per call unless eager-loaded (N+1 in loops); flagged()/notFlagged() are almost certainly whereHas/whereDoesntHave subqueries whose cost scales with the global flags table, which is shared across all flaggable models; and there is no retention story — the table grows monotonically with flag count × model count. Flag names are strings, so drift and typos are a real defect class (mitigated by the enum support added in 1.4.0 — mandate backed enums rather than literals). Ecosystem validation is thin: 454 stars is respectable but the metadata shows 0 dependents, so we are largely on our own for edge-case hardening. Mitigating factor: the surface area is tiny and MIT-licensed, so forking or vendoring is a credible fallback.

Key questions

  • Does the shipped migration include a unique index on (flaggable_type, flaggable_id, name), and what are the actual indexes on name?
  • What is the write semantics of a repeated flag() — upsert, insert-or-ignore, or update-then-insert? Is it atomic?
  • Does hasFlag() lazy-load the relation each call, and can flags be eager-loaded to avoid N+1?
  • What is the expected flags table size at 12/24 months, and who owns purging it?
  • Are there concurrency requirements beyond a single sequential command, and if so, do we need an external lock?
  • Which models and which team owns the flag-naming convention?

Integration Approach

Stack fit Native Laravel — Eloquent trait, service provider, publishable migration and config, database-agnostic through the query builder. No queue, cache, or external service dependency, so it slots into an existing Laravel app without infrastructure change. It composes cleanly with queued jobs, scheduler entries, and artisan commands, which is where the value is realised.

Migration path Adopt additively; nothing existing is modified. Two entry points:

  1. Greenfield — install, migrate, add the trait, start using it in new commands. Zero migration risk.
  2. Replacing ad-hoc state — where teams currently use boolean columns (sent_promo_mail), a bespoke processed_users table, or nothing at all, run a backfill that maps existing truth into flags, dual-write for one release cycle, then retire the old columns. Retiring a boolean column is a separate, reversible migration; the flags table itself can be dropped entirely if we abandon the package, which keeps lock-in low.

Compatibility Confirm the package's composer.json constraints against our pinned Laravel and PHP versions before committing — the changelog confirms Laravel 13 support but not the exact floor. Pin the minor version (currently 1.5.x) in composer.json and let routine dependency updates flow through normal PR review. The flag_model config is the only override knob; if we extend Flag, document it centrally so nobody re-registers a competing model.

Sequencing

  1. Spike (1 week, one engineer): install on a branch, read the published migration, answer the index/uniqueness questions above, and prototype the artisan-command idempotency scenario in a test.
  2. Conventions first: publish a short internal doc defining flag naming (backed enums, prefixed by domain), retention expectations, and the concurrency caveat. This prevents the string-drift problem before it exists.
  3. Pilot: apply to one non-critical backfill command with a measurable "no duplicate sends" acceptance test; instrument query counts to quantify hasFlag and scope cost.
  4. Broaden: roll out to remaining commands only after the pilot shows acceptable query profiles; batch/chunkById the scope queries.
  5. Review gate: at 90 days, decide keep/extend/retire based on table growth and incident count.

Operational Impact

Maintenance A small, single-purpose dependency with a clean changelog. Recent releases are dominated by dependency bumps plus occasional contributor features (enum support in 1.4.0, array unflagging in 1.5.0), which signals steady but low-velocity maintenance rather than active development. Budget near-zero ongoing engineering: version bumps ride normal dependency PRs. The one recurring task is deciding whether to prune the flags table.

Support MIT, community-supported, no SLA. Spatie is a well-known Laravel vendor, so community answers and source readability are strong; escalation path is "read the ~few hundred lines of source" or fork. Because it is a small, isolated package, a fork or a 200-line in-house reimplementation is a viable escape hatch if maintenance stalls — that optionality is the main reason the risk is acceptable.

Scaling Cost scales with rows, not with traffic. At n models × m flags, the flags table can reach millions of rows in a large user base, and the flagged()/notFlagged() scopes execute correlated subqueries against it. Mitigations: ensure a composite index on (name, flaggable_type, flaggable_id) and the reverse for relation loads; chunk long-running commands; avoid flagging high-frequency events; define a TTL or a periodic purge job for flags that only matter during a run; and prefer eagerly loading flags over repeated hasFlag() calls in loops. Under concurrent workers, do not rely on flags alone for exactly-once semantics — pair them with WithoutOverlapping middleware, a queue-level unique job, or a row lock.

Failure modes

  • Concurrent duplicate work: two workers both see notFlagged and both act. Impact: duplicate emails/charges. Mitigate with locking, not with flags.
  • N+1 query storms: hasFlag() inside a loop over thousands of records. Impact: latency, DB load. Mitigate with eager loading and chunking.
  • Unbounded growth: no purge ever scheduled. Impact: slow scopes, bloated backups. Mitigate with retention policy and owned cleanup command.
  • Orphan rows: flags left behind if models are hard-deleted outside the trait's delete hook (added in 1.1.0) or via raw SQL. Impact: table bloat and misleading counts. Mitigate with a reconciliation job.
  • Name drift: typo'd string flags silently never match. Impact: silent non-idempotency. Mitigate by mandating backed enums and code review.
  • Package abandonment: low likelihood, low blast radius; fallback is fork or reimplement.

Ramp-up Very low. Any Laravel developer is productive in under an hour; the only non-obvious knowledge is the concurrency caveat and the eager-loading behaviour. The real ramp-up cost is organisational: getting teams to agree on flag naming and to stop inventing per-feature boolean columns. A one-page convention doc at pilot time is the highest-leverage artefact here.

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.
socialdept/atp-orm
cboxdk/laravel-assistant-client
socialdept/atp-resolver
socialdept/tenant-domains
tautvydasr/blade-class-names
socialdept/color-scales
maherelgamil/controller
coleus/widgets
coleus/table
coleus/users
coleus/music
coleus/health
coleus/calendar
coleus/notes
coleus/support
coleus/settings
coleus/apps
seampact/laravel
emmyconceptng/laravel-ng-postcode
jjsquady/round-robin