protonemedia/laravel-eloquent-scope-as-select
Laravel Eloquent Scope as Select lets you add local scopes as select subqueries, so aggregated or computed values from related models come back in a single query. Skip extra database hits and N+1 problems while keeping your models clean and readable.
What the package actually does. protonemedia/laravel-eloquent-scope-as-select is a query-construction utility, not a framework component. It lets an existing Eloquent scope or constraint closure be appended to a parent query as a correlated subquery — typically as a derived column (SELECT ... , (SELECT ...) AS alias) or as a filter — so the scope's logic lives in one place instead of being re-implemented as addSelect, withCount, or raw SQL. The value proposition is DRY-ness across the codebase, not new database capability.
Architecture fit. Narrow and correct for a Laravel monolith or modular Laravel app that already centralizes query logic in scopes. It sits entirely at the Eloquent builder layer: no service provider bootstrapping beyond package discovery, no migrations, no schema, no runtime middleware, no queue or cache interaction. That means it is architecturally inert — it cannot destabilize the app's structure, and it can be removed without data consequences. Fit is poor for teams that do not use scopes heavily, that favor native withCount/withExists/withSum (Laravel 9+), or that maintain a query-builder abstraction in another language/ORM.
Integration feasibility. High. Composer install plus auto-discovery; adoption is per-call-site and reversible. The real feasibility question is not "can we install it" but "does the generated SQL perform." Because the package emits correlated subqueries, the integration effort is dominated by query review, not by wiring. Budget a short spike: install, convert one representative scope, capture toSql() with bindings, run EXPLAIN, compare against the current hand-written equivalent. If that passes on a realistic data volume, rollout is cosmetic. If it fails, the failure is visible immediately and cheaply.
Technical risk.
whereRaw/selectRaw, orderByRaw, or nested where groups is the most likely source of subtle wrong-result or Invalid parameter number errors. Any adoption needs integration tests that assert generated SQL, not just result counts.where('active', true) across joined tables) will produce ambiguous-column errors or silently pick the wrong table.orderBy/limit inside a subquery differ by context (scalar select vs. whereIn vs. exists). Scopes written for top-level use may not be valid as-is; this is exactly the kind of thing to enumerate before committing.withCount would be materially faster.Key questions for the spike.
groupBy, having, or orderBy+limit?addScopeAsSelect signature, trait vs. builder macro, alias handling) in the pinned version, and is it stable across the Laravel versions we run?EXPLAIN in CI?withCount/withExists/native addSelect sufficient for the majority of our cases, leaving only a handful that genuinely need scope reuse?Stack fit. Laravel 9/10/11, PHP 8.1+, MySQL/PostgreSQL/SQLite. Fits cleanly into an existing Eloquent-centric codebase with a repository or query-object layer. It does not fit applications that route reads through a non-Eloquent layer (raw query builders, stored procedures, a read model in another service), since the package is only meaningful when scopes are the unit of reuse.
Migration path. There is no data migration. The migration is conceptual: move duplicated constraint logic out of PHP-side ID arrays and bespoke addSelect closures and back into the canonical scope. A pragmatic three-step path:
DB::listen or Telescope, capture execution time and EXPLAIN, compare against baseline.Compatibility. Pin the exact version in composer.json, commit the lockfile, and add a composer outdated check to the upgrade runbook. Verify against our current Laravel minor before adoption and again before each Laravel major. If the package lags a Laravel release we need, the fallback is a vendored fork or replacement with native addSelect — which is why the calls should be isolated.
Sequencing. Do this after any in-flight Laravel upgrade lands, not concurrent with it, to keep the variable count at one. Adopt in read paths first; keep it out of write/transaction paths and out of anything on a latency SLO until measured. Roll out behind a repository method so that swapping the implementation for native Eloquent is a single-file change. Order of work: spike (1–2 days) → ADR → shadow tests → one endpoint → documented pattern → broader use.
Isolation pattern. Wrap every call site in a small internal helper (e.g., App\Query\ScopeSubquery::select(...)) rather than calling the package fluently throughout the codebase. This costs almost nothing and converts an unbounded dependency into a one-file dependency. It also gives a natural place to log generated SQL and enforce qualified columns.
Maintenance. Low ongoing effort: a small, stable, MIT-licensed library with no configuration, no runtime services, and no migration burden. The maintenance risk is upstream, not internal — the library must track Eloquent internals. With one maintainer and no dependents, treat it as "likely fine, but budget a fork." The whole package is small enough that an internal fork with a couple of regression tests is a realistic contingency, and that contingency should be written down rather than assumed.
Support. No SLA, GitHub issues only. Internal support comes from the ADR plus a short runbook: how to inspect generated SQL, how to add a scope, what patterns are banned (unqualified columns, orderBy inside a scalar subquery, use in hot paths without EXPLAIN). Assign one owner on the platform team so questions have a destination.
Scaling. Behavior is entirely a function of the database. Correlated subqueries execute per row of the outer result set; on a large outer set without supporting indexes this degrades badly and non-linearly with data growth — and it will degrade quietly, because the application code does not change. Mitigations: restrict to queries with bounded outer result sets (paginated lists, single-record lookups); require index coverage on the correlation columns; prefer native withCount/withExists where they express the same thing; and use denormalized counters for anything on a hot path or an SLO. Capacity review should include these queries explicitly, since they will not show up as N+1 in the ORM layer and are easy to miss in profiling.
Failure modes to watch.
orderBy/limit/groupBy is used in a context it was not written for.Ramp-up. Low. Any developer fluent in Eloquent can use it in under an hour. The non-obvious skill is reading the generated SQL and knowing when not to use it — that is a 30-minute brown-bag session plus the runbook, not a training program. Onboarding risk is that developers treat it as a performance tool rather than a DRY tool; name that explicitly in the ADR.
Recommendation. Adopt conditionally: proceed if the spike shows acceptable plans on realistic data, if call sites are isolated behind an internal helper, and if the team accepts a documented fork-or-replace fallback. Do not adopt if native withCount/withExists/addSelect already covers the duplication problem, or if the target queries sit on latency-sensitive paths where a denormalized counter is the better answer. The package solves a real but modest hygiene problem; it is not worth carrying on architectural grounds alone.
How can I help you explore Laravel packages today?