mpyw/laravel-local-class-scope
A tiny Laravel macro that lets you reuse global scope classes as local query scopes. Call User::scoped(ActiveScope::class) or pass constructor arguments, chain scopes, and optionally wrap them in model methods or traits. Includes a PHPStan extension for static analysis.
Architecture fit. The package is a single auto-discovered macro that adds scoped() to Eloquent query builders, accepting either a Illuminate\Contracts\Database\Eloquent\Scope class-string, an instance, or a class-string plus constructor arguments. It introduces no new contract, service provider to publish, config, or migration — the only abstraction engineers must learn is Laravel's existing Scope interface. Architecturally this is the lightest possible shape for the capability: it reuses the same Scope classes already used for global scopes, so there is no parallel scope dialect to maintain. It aligns with laravel/ideas#636, meaning the idiom is directionally consistent with where the framework has publicly discussed going.
Integration feasibility. High. composer require plus auto-discovery gets a working macro in one minute. The real work is codebase-side: inventorying existing app/Scopes/ classes and duplicated predicates, then refactoring call sites. There is a bundled PHPStan extension for scoped() typing, which is the difference between adopting this safely and adopting it with suppressions; the ordering constraint (this package's extension.neon before Larastan's) is a concrete, testable setup step, not a vague caveat.
Technical risk. The dominant risk is provenance, not code. 23 stars, zero dependents, a single maintainer, and at least one release (v2.2.0) that is a version bump with no functional content. The dataset's 2026-06-13 release timestamp is future-dated and must be verified against the repository before any planning assumption rests on it — the difference between "stable by design" and "under-invested" changes the recommendation. Secondary risks are technical and enumerable:
scoped() macro is a shared namespace; another package today or a future Laravel method of the same name produces an ambiguous-call upgrade hazard.remove() on other scopes, assume single invocation, or depend on booted() timing. Applied locally and repeatedly, they can behave surprisingly.^8.2 and Laravel ^11–14. On Laravel ≤10 this is a blocker, not a trade-off. It also means the package must keep pace with Laravel majors — a single maintainer is the constraint on that.Key questions. Who owns the macro internally if upstream goes quiet, and is vendoring under MIT acceptable to procurement/security? Does any current or planned dependency register scoped() on the builder? Are our candidate global scopes idempotent and free of remove()/boot-timing assumptions when applied locally? Does the PHPStan extension ordering actually work in our phpstan.neon without suppressions? What is our Laravel upgrade cadence relative to the maintainer's release cadence?
Stack fit. This is a first-filter decision: Laravel 11–14 with PHP 8.2+. Teams on Laravel ≤10 should defer outright. Teams on the supported matrix and already using class-based Scope objects are the natural fit; teams with only closure scopes or dynamic request-driven filters will get less value and should not pilot.
Migration path. There is no data or schema migration — this is a code refactor with a cheap reversal. Extract a duplicated predicate into App\Scopes\XScope implementing Scope, replace inline clauses with Model::scoped(XScope::class) or the parameterized form, and keep the diff reviewable. The fallback path is removing the package and inlining or wrapping the call sites, so the decision is reversible at any point. Avoid a repo-wide sweep: backfill only where a predicate is duplicated three or more times, because a sweep maximizes the surface to unwind later.
Compatibility. Three concrete compatibility surfaces: (1) the scoped macro name against other packages and future framework methods; (2) PHPStan/Larastan extension ordering; (3) the package's Laravel version constraint against your upgrade cadence. The native-Laravel migration story is favorable — if the ideas proposal lands, moving to the framework equivalent should be mechanical — which lowers the cost of adopting now and the cost of being wrong.
Sequencing.
app/Scopes/ and booted()/addGlobalScope() registrations; audit candidates for local-use safety (remove(), single-invocation assumptions, boot timing).Maintenance. Low ongoing code maintenance — the package is a small, forkable MIT macro with a narrow surface, and it can be vendored internally if upstream stalls. The maintenance burden is instead organizational: pinning a version, watching upstream releases against Laravel majors, and owning the scoped idiom internally (naming, app/Scopes/ conventions, argument-passing style, chaining and precedence documentation). A single-maintainer, zero-dependent package means the bus factor is effectively one, and that should be written down as an accepted risk rather than discovered later.
Support. No commercial support. Issues are handled by the maintainer on a best-effort basis. Teams with vendor-support or procurement policies requiring adoption evidence will fail this package on 23 stars and zero dependents — that is a legitimate outcome, not an objection to be argued past. Internal support means nominating an owner for the macro and the scope conventions.
Scaling. Runtime cost is negligible: the macro delegates to the same builder the scope would have modified. The scaling risk is behavioral, not performance — every additional predicate silently changes query plans and index usage, and caching layers keyed on query shape will see different shapes. Parameterized scopes can also be misused to build filters dynamically from request payloads, which reintroduces the abstraction cost the package was meant to remove.
Failure modes.
scoped() → ambiguous call or hard failure on upgrade.remove()-calling scope applied locally → unexpected scope removal or duplicated predicates.Ramp-up. Minimal for any engineer already familiar with Scope. The three call styles (Class::class, new Scope(...), Class::class, ...args) are learnable from the README in minutes, and the PHPStan extension makes IDE-level discovery work. The genuine ramp-up cost is team convention: where scopes live, whether they take constructor arguments, whether to expose scopeX() wrappers or traits, and how chaining and precedence are documented. Budget a sprint for the pilot to surface that, and treat engineer preference for the idiom over closures as an explicit success criterion rather than an assumed one.
How can I help you explore Laravel packages today?