paulhenri-l/eloquent-builder-decorator
A Laravel package for decorating Eloquent query builders. Wrap builders to add reusable query logic, filters, and composable behavior without touching your models. Keeps query customization clean and testable.
Architecture fit
Illuminate\Database\Eloquent\Builder is the primary query surface in virtually every Laravel application. This package targets that surface with a decorator/wrapper pattern: a class holds a builder instance and proxies __call/fluent methods through to it, returning the decorator so custom, named query methods can be chained with full type fidelity.public function scopeActive(...)), Builder::macro(), and a first-class custom builder via Model::newEloquentBuilder() / Model::newQuery(). The decorator is a fourth path. Its unique value is IDE-friendly, statically analyzable, type-safe fluent methods without mutating the model or global Builder state — a real gap versus macros and scopes.Integration feasibility
#[ReturnTypeWillChange], and static-return types) and Laravel 8→11 changes to ForwardsCalls, Conditionable, and builder internals make compatibility a verification task, not an assumption.Builder API surface, it is inherently fragile against upstream additions — every Laravel minor release can add builder methods whose return semantics the wrapper may not model correctly.Technical risk
whereHas, with, relationship builders, paginate, chunk, cursor, find) return concrete Builder/Relation/LengthAwarePaginator instances. A wrapper cannot intercept all of these, so chains will silently "unwrap" back to the raw builder, defeating the type promise and producing confusing errors.__call-proxied methods are often invisible to Larastan/PHPStan and IDE autocomplete unless the decorator uses @mixin or explicit stubs; the package's typing story needs validation.Builder will reject the decorator, forcing either Builder-returning escape hatches or pervasive signature changes.Key questions
ForwardsCalls/__call, and is it @mixin-annotated for PHPStan/IDE support?Model::query() replacement) and per-model registration, or is it caller-constructs-only?whereHas closures, chunkById, and pagination?Stack fit
Migration path
UserQuery::make(User::query()) or an equivalent factory) rather than globally replacing newQuery(). This keeps the blast radius to opted-in read paths and leaves writes, relations, and existing scopes untouched.Compatibility
ForwardsCalls-based decorator plus @mixin annotations and tests pinning the builder methods actually used.Sequencing
where/orderBy/paginate, and static analysis.newEloquentBuilder() subclass on the same three or four real queries. Pick the lowest-abstraction option that meets the typing goal.Maintenance
Support
Scaling
Failure modes
chunk/cursor return concrete types, so chains lose the decorator and later custom methods fail or fall through to BadMethodCallException.Illuminate\Database\Eloquent\Builder is expected throws TypeError.@phpstan-ignore noise.Ramp-up
TPM recommendation
How can I help you explore Laravel packages today?