cybercog/laravel-eloquent-flag
Add boolean flags to any Eloquent model with cybercog/laravel-eloquent-flag. Mark records as flagged, unflag or toggle them, and query flagged or unflagged rows using clean, expressive methods and local scopes—no extra boilerplate needed.
Architecture fit. Trait-composition only: no service provider, no container bindings, no publishable config, no shipped migrations. The package adds nothing to the application boot cycle, which makes it trivially adoptable and trivially removable. The design is two-dimensional — boolean (is_published) vs timestamp (published_at) storage, crossed with classic (default query returns flagged records) vs inverse (default query returns unflagged records) scope direction. That maps cleanly onto the lifecycle states most Laravel apps hand-roll: draft/published, pending/approved, active/expired, kept/discarded.
The important framing for planning: this is a behavior layer, not a workflow engine. It gives publish() / undoPublish() / isPublished() / isNotPublished(), a handful of query macros (withNotPublished(), onlyNotPublished()), and two model events (published, publishedUndone). Approval chains, permissions, and transition guards remain ours.
Integration feasibility. High. Columns plus one trait import plus an optional shouldApplyXxxScope(): bool method per model. No vendor publish step, no config file, no queue worker, no scheduled task. Composer install and go. The only design decision that carries weight is whether the scope is applied — and it is opt-in per model, which is the package's single best safety property and also the thing most likely to be applied without thinking.
Technical risk.
shouldApplyXxxScope() returns true, every Eloquent query on that model silently excludes unflagged rows: admin panels, exports, aggregates, relationship eager loads, API endpoints. Inverse variants invert the default. Reports quietly returning fewer rows than reality is the failure mode that erodes trust in the tooling.DB::table(), toBase(), joined tables, and withoutGlobalScopes() all bypass the behavior. Any BI or reporting layer built on the query builder will disagree with the ORM, silently.published_at answers "when," never "who" or "why." Undo leaves no trace unless we listen to publishedUndone ourselves.HasKeptFlag performs writes you didn't ask for — sets is_kept = false on create and true on update. Any model with existing observers needs verification of ordering.Key questions.
Stack fit. Laravel 9–13, PHP 8.0–8.5 covers essentially any current Laravel estate, including one mid-upgrade. Composer-only, MIT, PSR-12, declare(strict_types=1) throughout — it will pass an existing style gate. Note that the package's own suite runs on Orchestra Testbench with in-memory SQLite, so its guarantees are framework-level, not database-level; Postgres/MySQL behavior, index plans, and collation edge cases are ours to validate. That's normal for a package of this shape but should be stated explicitly to whoever owns the data layer.
Migration path. Purely additive — no rewrite, no data migration tooling required.
is_published boolean default false, published_at nullable timestamp) alongside existing state columns.shouldApplyXxxScope(); verify writes, casts, and events fire correctly.The step that changes behavior is #4, not #3. Treat it as a release, not a refactor.
Compatibility considerations.
withoutGlobalScope in resources — never both inconsistently.HasKeptFlag must not be attached to models with write observers until ordering is confirmed.Sequencing.
Article-class) end-to-end in a branch; capture query plans and index requirements.published / publishedUndone into our audit log if attribution is needed.Verdict. Adopt for lifecycle state on a bounded, explicitly enumerated set of content models. Do not adopt as the system of record for compliance-critical state without wrapping it, and budget a fork (MIT makes this free) once the model list stabilizes.
Maintenance. Low steady-state effort, high conceptual care. No runtime service, no queue, no cron, no migrations to run. Upgrade cost tracks Laravel majors and the support matrix suggests the package keeps pace — budget one verification cycle per Laravel major rather than continuous attention. The asymmetry to flag: using the package is nearly free, extending it is not. Resist upstreaming custom flags.
Support. Self-serve. Small community, single-vendor namespace, no commercial backing. Mitigation is cheap and should be pre-approved rather than deferred: vendor the source into our own namespace or a private Composer VCS once the adopted model list is final. Roughly a day of work that converts abandonment risk from a project to a chore.
Scaling. No overhead beyond the WHERE predicate each applied scope introduces. Performance is entirely an indexing question. Design composite indexes around the columns actually filtered and sorted on (is_published, published_at, deleted_at-style) rather than indexing the boolean alone, which on a mature table gives the planner little to work with. No N+1, memory, or connection-pool characteristics are introduced by the package itself.
Failure modes to instrument.
DB::table() counts, run in CI.HasKeptFlag touching is_kept on create/update.Ramp-up. Low for any mid-level Laravel developer — idiomatic traits and global scopes, roughly half a day to read the source of the two or three flags actually in use. The real cost is team-wide comprehension of scope semantics, which is a documentation and code-review problem rather than a training problem. Budget a short internal write-up ("which models are scoped and why") and make every withoutGlobalScope call an explicit, reviewed decision instead of an ad-hoc escape hatch. Realistic onboarding: one sprint for the first model end-to-end, near-zero marginal cost per model thereafter.
How can I help you explore Laravel packages today?