Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Eloquent Builder Decorator Laravel Package

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.

View on GitHub
Deep Wiki
Context7

Getting Started

Install

composer require paulhenri-l/eloquent-builder-decorator

There is no config file to publish and no migrations. Check composer.json for an extra.laravel.providers entry — if one exists it is auto-discovered; otherwise there is nothing to register.

Where to look first

The package is intentionally tiny (a trait plus an abstract decorator class). Spend ten minutes in two places:

  1. src/ — read the trait and the abstract decorator top to bottom. That is the package. Confirm the exact class/trait names and the property used to declare decorators, since these vary between the README and the source in older micro-packages.
  2. tests/ — the test suite is the most reliable usage documentation for a package this small.

First use case

You have a fat model with a dozen scopes, and you want the same query vocabulary available on several models without copy-pasting scopes. Add the trait to the model, list your decorators, and the model's builder is wrapped transparently:

use PaulhenriL\EloquentBuilderDecorator\CanDecorateBuilder;

class User extends Model
{
    use CanDecorateBuilder;

    protected $builderDecorators = [
        \App\Builders\Concerns\Publishable::class,
        \App\Builders\UserBuilder::class,
    ];
}
namespace App\Builders;

use PaulhenriL\EloquentBuilderDecorator\BuilderDecorator;

class UserBuilder extends BuilderDecorator
{
    public function active(): self
    {
        return $this->where('active', true);   // forwarded to the wrapped builder
    }

    public function createdSince(\DateTimeInterface $date): self
    {
        return $this->where('created_at', '>=', $date);
    }
}

Now every entry point into the builder is decorated:

User::active()->createdSince(now()->subMonth())->get();
User::query()->active()->paginate(25);
$user->posts()->active()->get();          // related model's builder is decorated too

Verify it took effect with dd(get_class(User::query()));.

Implementation Patterns

Return $this, never the raw builder. Every decorator method should mutate through the forwarded call and return $this so the chain stays on the decorator. Returning the underlying builder silently drops your custom methods for the rest of the chain.

Keep decorators stateless and additive. A decorator should only add named query fragments. If a decorator needs constructor state, it will not survive the package's wrapping/cloning behaviour — pass state through method arguments instead (e.g. ->ofType($type)), not through properties.

Use one decorator per concern, then mix them. A Filterable decorator that maps request input to where clauses, a Sortable decorator, and a domain decorator (UserBuilder) can each live in app/Builders/Concerns and be attached to any model. This is the package's real value: composition of builder behaviour across models, which a per-model custom Eloquent\Builder subclass cannot give you without inheritance gymnastics.

Decorator order is composition order. Each decorator wraps the previous one, so method resolution goes last-declared-first. If two decorators define the same method, the outermost wins. Keep the list ordered from most generic to most specific.

Prefer decorators for cross-model logic, scopes for model-local logic. A scope that references model-specific columns belongs on the model. A where('published_at', '<=', now())-style fragment reused by Post, Page, and Article belongs in a decorator.

Eager loading and relationship closures. Constraint closures receive the decorated builder, so this works:

Post::with(['comments' => fn ($q) => $q->approved()->latestFirst()])->get();

Testing. Decorators are plain objects and easy to test against SQLite:

$query = (new UserBuilder(User::query()->getQuery()))->active();
$this->assertStringContainsString('"active" = ?', $query->toSql());

Or test end-to-end against a RefreshDatabase SQLite connection — cheaper and less brittle than asserting SQL strings.

IDE and static analysis. The decorator is invisible to autocompletion. Add @method annotations on the model (or a @mixin docblock pointing at the decorator) so PHPStan/Psalm and your IDE see the fluent API:

/**
 * @method static static|\App\Builders\UserBuilder active()
 * @method static static|\App\Builders\UserBuilder createdSince(\DateTimeInterface $date)
 */
class User extends Model { /* ... */ }

Gotchas and Tips

Trait/method collision on newEloquentBuilder. If your model already overrides newEloquentBuilder(), the trait's implementation will conflict (fatal error) or your override will bypass decoration entirely. Resolve with insteadof, or drop the trait and wrap manually in your own override — it is only a couple of lines.

Pass-through return values are the #1 breakage. Any decorator method that delegates ($this->where(...), $this->orderBy(...)) relies on the base class forwarding calls back to the decorator. If a forwarded method returns the underlying builder instead, User::query()->where(...)->active() fails with "method does not exist". Test chains that start with a stock builder method, not just chains that start with your decorator.

getQuery() escapes decoration. $builder->getQuery() returns the raw Illuminate\Database\Query\Builder, so anything you do after it is undecorated and invisible to your custom methods. Same for code that reaches into ->getQuery()->where(...) in third-party packages.

Type hints against Eloquent\Builder will fail. If the decorator is a wrapper (not a subclass), function (Builder $q) closures and packages that type-check relationships will throw TypeError. Audit anything that passes the builder to a typed callable; use the decorator's own type or drop the hint.

Not everything is forwarded. __call forwarding typically does not cover __get, __clone, offsetGet/ArrayAccess, or Countable. $builder->count() usually works; $builder['id'] and deep-cloning may not. Verify before relying on them.

Model static calls are fine, but check the edges. User::where(...), User::firstOrCreate(...), and chunk/cursor all route through newQuery() and therefore through your decorator. User::create() does not touch the builder at all — decorators cannot influence it.

Version drift is the real risk. Last release is April 2020 (Laravel 6/7 era), 11 stars, zero dependents, no maintainer activity. Check the illuminate/* constraint in composer.json before installing on Laravel 10/11; expect to widen it yourself and to hit PHP 8 return-type/static covariance issues. Debugging symptoms are usually "class not found" during resolution rather than a clean error — composer why-not and dd(get_class(User::query())) are your first moves.

Consider whether you need the dependency at all. Laravel's native newEloquentBuilder() covers single-model custom builders, and Builder::macro() / Macroable covers cross-model reusable query fragments with zero dependencies. This package wins narrowly when you want stacked, class-based, autocompletable decorators reused across many models with per-model ordering. The whole mechanism is roughly ten lines of code — vendoring it into app/Support removes the maintenance risk entirely and is a reasonable default given the package's age.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
elmogy/larafast
xeviant/laravel-paystack
gorilladash/laravel-website-sdk
martialbe/flysystem-oss
chondal/3312client
jeroen-g/flickr
opscale-co/nova-genbi
itsjustvita/laravel-retention
tequilarapido/trackit
gregsphotography/site-builder-filament
cabaoctavio/laravel-query-log
gregsphotography/site-builder-components
io-digital/clickatell
snowsoft/laravel-admin
oxygen/auth
relicsoftware/barakah-payment-gateway
tobya/work-with-composer
equipc/laravel-email-validator
equipc/laravel-email-database-log
adedsec/nova-translated-labels