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 Macros Laravel Package

exyplis/eloquent-builder-macros

This Laravel package adds reusable macros to Eloquent’s query builder, making common filtering, ordering, and query-building tasks shorter and more expressive. Keep models and repositories cleaner by replacing repetitive query code with handy, chainable macros.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and verify registration

composer require exyplis/eloquent-builder-macros

The package ships a service provider that is auto-discovered on Laravel 5.5+. It registers macros onto Illuminate\Database\Eloquent\Builder (and in most cases the underlying Illuminate\Database\Query\Builder). No config, no migrations, no publishing step.

Where to look first

  1. vendor/exyplis/eloquent-builder-macros/src — the provider and the macro definitions are the actual documentation. With a package this small there is no manual; read the Builder::macro(...) closures directly to learn exact signatures.
  2. Enumerate what you actually have at runtime before writing code against it:
foreach (['whereLike', 'orWhereLike', 'whereStartsWith', 'whereEndsWith', 'whereDateBetween'] as $m) {
    dump($m, \Illuminate\Database\Eloquent\Builder::hasMacro($m));
}

First real use case

The archetypal win is a search endpoint. Instead of scattering wildcards through controllers:

// Before
$query->where('name', 'like', "%{$term}%")
      ->orWhere('email', 'like', "%{$term}%");

// After
User::query()->whereLike(['name', 'email'], $term)->get();

Start by replacing one repetitive where(... 'like', "%...%") clause in a scope, confirm the generated SQL via ->toSql(), then expand.

Implementation Patterns

Search and filter scopes

Wrap macros in local scopes so the macro never leaks into controllers and so call sites stay refactorable:

public function scopeSearch(Builder $q, ?string $term): Builder
{
    return $q->when(
        filled($term),
        fn (Builder $q) => $q->whereLike(['name', 'email', 'company'], $term)
    );
}

This composes cleanly with when() and keeps the "macro exists" dependency at one layer.

Layering conditional filters

The natural workflow is: query builder macros handle predicate construction, when() handles presence, and scopes handle reuse.

Post::query()
    ->when($request->q, fn ($q, $v) => $q->whereLike('title', $v))
    ->when($request->from, fn ($q, $v) => $q->where('created_at', '>=', $v))
    ->when($request->from && $request->to, fn ($q) => $q->whereDateBetween('created_at', $request->from, $request->to))
    ->paginate();

Integrating into existing query objects

Macros return the builder, so they slot into any chain, including inside whereHas closures and relationship constraints:

User::whereHas('posts', fn ($q) => $q->whereLike('title', $term))->get();

Because macros are bound to the builder instance, $this inside the closure is the builder itself — a closure written as fn ($col, $v) => $this->where(...) works, but prefer the explicit fn (Builder $b, ...) parameter form for readability.

Testing

Assert on SQL rather than results — it isolates the macro contract from database state:

$sql = User::whereLike('name', 'foo')->toSql();
$this->assertStringContainsString('"name" like ?', $sql);

If you depend on a specific macro set across many tests, add a single smoke test asserting Builder::hasMacro('whereLike') so a package upgrade that renames or drops a macro fails loudly in CI rather than at runtime.

Vendoring as an escape hatch

With zero dependents and no release history to speak of, a defensible pattern for anything load-bearing is to copy the macro closures into a AppServiceProvider::boot() or a dedicated QueryMacros class. You lose updates you were unlikely to get, and you gain the ability to add types, tests, and IDE docblocks.

Gotchas and Tips

Macros are global and collision-prone

Registering whereLike overrides any other package or app-level macro with the same name, silently and load-order-dependently. Check Builder::hasMacro('name') before defining your own, and audit composer show for other macro packages before introducing this one.

Query vs. Eloquent builder

A macro registered only on Illuminate\Database\Eloquent\Builder will not be available inside DB::table(...) chains or in some raw query contexts. If a macro seems to "not exist" in one place and work in another, this is almost always why. Register on both classes if you need parity.

like wildcards and index usage

Any macro that prepends % to the term disables B-tree index usage. On tables beyond a few hundred thousand rows, switch to a full-text index or whereFullText, or accept the scan knowingly. Also confirm whether the package escapes % and _ in the search term — user input containing % otherwise becomes a wildcard and can return the whole table.

Case sensitivity is a collation concern, not a macro concern

whereLike does not normalise case. On MySQL with utf8mb4_general_ci it's case-insensitive; on Postgres it is case-sensitive and ILIKE is what you want. Verify what the macro emits per driver — a package written against MySQL defaults may produce surprise behaviour on Postgres or SQLite.

SQL injection surface

Macros built with DB::raw("... $value ...") interpolation are injectable even though they look like builder methods. Read the closure bodies before trusting a macro with request input; prefer ones that pass values as bindings.

Static analysis and IDE support

Macros are invisible to PHPStan/Psalm and to IDE autocompletion — User::whereLike() will be flagged as an undefined method. Options: add a @method docblock to a base model or a _ide_helpers stub, install barryvdh/laravel-ide-helper, or write a PHPStan stub. Expect to do one of these on day one or your static analysis baseline will regress.

Extension points are thin

There is no config file, no driver abstraction, and no event system — the only extension point is Builder::macro() itself. If you need a variant (say a whereLike that lowercases), define a new macro name rather than trying to monkey-patch, and keep it in your own provider so it survives a composer update.

Version pinning

Given the package's maturity, pin an exact version ("exyplis/eloquent-builder-macros": "1.0.*") and commit the lockfile. A patch release that changes a macro's argument order or adds a default % wrapper would otherwise reach production untested.

Debugging checklist

When a macro misbehaves: ->toSql() with ->getBindings() reveals exactly what was built; Builder::hasMacro() confirms registration; app()->getLoadedProviders() confirms the service provider booted (relevant if you disabled package discovery); and a DB::listen() dump confirms the macro ran at all versus being short-circuited by a when() returning false.

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.
abbasudo/artisan-route
mirvan/ippanel
ignibyte/aic-framework
glavweb/data-schema-bundle
glavweb/datagrid-bundle
tacticmedia/scim-server
uni-method/frame
neika-ai/sdk
adachsoft/php-code-style
pharaonic/laravel-notification-dependencies
beautinow/translug
dev-brainiac/bot-cloaked
octoberrain/html
octoberrain/extension
octoberrain/exception
octoberrain/contracts
adachsoft/api-version-bundle
cgv/localized-resource
noith/settings
kda/laravel-package