beyondcode/laravel-scope-checks
Laravel Scope Checks offers simple helpers for verifying that an Eloquent model has a given query scope before you apply it. It helps keep dynamic query building safe and predictable, with readable checks that are easy to use and test.
Architecture fit. This is a developer-ergonomics shim over Eloquent local scopes, not a domain capability — one trait plus reflection over scope* methods. The TPM-relevant framing is that it is a coding pattern with a reference implementation, not a component to procure. It fits small-to-mid Laravel monoliths, internal admin tooling, and legacy CRUD apps where the same inline status idiom (whereKey($id)->active()->exists()) is retyped across controllers, Blade templates, and API resources. It does not fit layered architectures with repository or query-object boundaries, read/write-separated models, or any contract where a boolean method silently issuing a database query is a violation. Blast radius is small and replaceable; the value delivered is consistency of status vocabulary, not new capability. As a TPM, that is the decision this actually informs: do we have one named definition of "active," "published," "verified" — yes or no.
Integration feasibility. Mechanically trivial (one composer require, one use statement, MIT, roughly 40 lines to reproduce). As a maintained dependency, feasibility is low: archived upstream, last release 2020-02-06, 77 stars, zero dependents, and no evidence of validation against Laravel 9/10/11 or PHP 8+. Two integration paths exist and they are not equivalent — vendor the archived package (implicitly owning patching forever, with supply-chain residue) or internalize the pattern (one afternoon, full ownership, no residue). Only the second is defensible for new product work, and it is the only one a TPM should fund.
Technical risk. High for production, and the dominant risk is silent incorrectness, not outage. A boolean returns a wrong answer inside a UI conditional or eligibility check with no exception to catch — strictly worse than a crash because it generates no signal and no page. Contributing factors: unvalidated framework compatibility, zero-argument-scope limitations, misleading results for non-filtering scopes (orderBy, with), unclear handling of global scopes and soft deletes, per-call query execution producing N+1 in loops and serializers, and collisions with existing same-named methods or accessors. The documentation is also internally inconsistent about shipped behavior — a description framing it as a query-assertion safety net versus technical assessments describing generated per-record predicates. Those are different products with different risk profiles, and we cannot fully specify what we would be adopting.
Key questions.
isActive() predicate — different call sites, different risk, different test strategy. Resolve by reading src/ before writing a single story.whereHas, and joins handled?false? (Only the loud failure is acceptable.)withExists(), attribute casting, or exists subselects? A "no, not needed" here is a legitimate and cheap outcome.Stack fit. Laravel apps using Eloquent local scopes directly, with non-critical, low-volume, internal checks — status badges, Blade conditionals, admin dashboards, feature-test assertions. Explicitly out of scope: authorization, billing, entitlements, compliance or regulatory status (those belong in Policies/Gates and explicit domain services), non-Laravel PHP, and teams standardizing on query objects. The one-line TPM test before funding anything: if a silently wrong boolean here would be worse than a hard failure, this pattern does not apply.
Migration path. Do not add this to composer.json. Fund a time-boxed spike (one to two days) on a branch pinned to an exact tag, against two or three representative models that already carry the inline logic. If the value holds, fork the MIT-licensed code into an internal namespace, add parity tests, and document ownership. If it does not, write a short engineering note capturing the pattern and close the ticket. The decision gate is the only roadmap artifact here, and it should not occupy a roadmap line of its own — it belongs inside a story that already requires status consistency (a dashboard or admin rework), never as standalone work.
Compatibility. Treat the release date as a hypothesis, not a guarantee. Validate Composer constraints, the Laravel/PHP matrix, and PHPUnit or Pest versions if used in tests. Exercise parameterized, nested, and global scopes; soft-deleted records; relationship constraints; scopes that add joins or whereHas; unsaved models; and models with an existing same-named accessor.
Sequencing.
Dependencies and gates. No cross-team dependencies, no vendor negotiation, no procurement cycle. The only gating items are (a) resolution of the behavioral ambiguity and (b) confirmation that native Eloquent features are not already covering the ground. Both are same-week, engineer-owned questions.
Maintenance. Upstream is archived — maintenance fully transfers to us. The MIT license and small surface make a fork realistic, but there is no release process, no issue triage, and no upgrade path. Budget it as a permanent line item on the Laravel upgrade regression checklist, not a one-time install. This is the single largest ongoing cost and it is small but non-zero, which is precisely why it should be scoped and named rather than absorbed invisibly.
Support. No vendor, no active community, and zero dependents means no external signal if it breaks. Internal on-call and the Laravel upgrade owner absorb every failure. Record in the risk register why the code lives in-house rather than in composer.json, so future maintainers do not helpfully reintroduce the package. One paragraph of rationale prevents a repeat decision in eighteen months.
Scaling. Per-call cost is tiny but is likely a database round trip, which becomes N+1 in Blade @foreach loops, API resources, and accessor-appended checks on paginated collections. For hot paths, prefer hydrated boolean columns, withExists() subselects, or cached flags so the predicate reads an already-loaded attribute. No horizontal scaling concern beyond ordinary database load. The TPM takeaway: this pattern is safe for single-record presentation logic and unsafe for anything list-shaped, and that boundary should be written down, not discovered in production.
Failure modes. Silent false positives/negatives in business logic (the dangerous case, invisible in logs and metrics); breakage on framework upgrade as Eloquent internals shift; BadMethodCallException when a scope is refactored away (the loud, safe case); incorrect semantics for non-filtering or parameterized scopes; collisions with hand-written methods; query storms from accessor-appended checks. Any internal version should be designed so the loud failure is the only failure mode — fail closed, never return a plausible false.
Ramp-up. Hours for any Laravel developer fluent in Eloquent scopes. The real cost is governance and verification: parity tests, call-site review, scope naming conventions, upgrade regression testing, and an explicitly named fork owner. Budget engineering for the verification and product/TPM for the decision gate — not for the code.
Recommendation. Use this as a reference pattern or a small internal utility, never as a production dependency. Archived status, a 2020 last release, zero dependents, and contradictory documentation are disqualifying for new product work. The genuine deliverable is a decision: adopt the status-vocabulary convention (one named definition of each state, expressed as a model predicate) and spend roughly an afternoon of engineering reimplementing it safely under our own namespace. Total ask: one to two days of spike plus a decision gate, zero roadmap impact, and a small permanent line on the upgrade regression list. The alternative — a frozen dependency inside production logic to save that afternoon — is a bad trade.
How can I help you explore Laravel packages today?