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

Eloquery Laravel Package

sehrgut/eloquery

eloquery is a small Laravel package that adds convenient helpers for building Eloquent queries, aimed at reducing boilerplate when filtering, sorting, and composing database queries in your models.

View on GitHub
Deep Wiki
Context7

Getting Started

Read this first: sehrgut/eloquery is archived, has 4 stars and zero dependents. Treat it as a small reference implementation of the "query string → Eloquent builder" pattern rather than infrastructure you build a product on. If you are choosing today, spatie/laravel-query-builder is the maintained equivalent with the same mental model. Use eloquery if you inherited it, if you need to read its source to copy the idea, or if you're evaluating it for a legacy codebase.

Minimal steps:

  1. Install and check the constraint. composer require sehrgut/eloquery. Because it's archived, expect the illuminate/* constraint to lag your Laravel version. --ignore-platform-reqs will get it installed and then blow up at runtime. If it doesn't resolve cleanly, fork it or vendor the src/ directory into app/Support/Eloquery — the package is small enough that this is a realistic option and removes the supply-chain risk entirely.
  2. Read these files, in this order: README.md → config/eloquery.php (this is where the query-parameter names and per-model allowlists live) → the single entry-point class in src/. The whole package is a few hundred lines; you can read all of it in one sitting, which is the main thing it has going for it.
  3. Publish the config so allowlists are visible in your repo: php artisan vendor:publish --tag=eloquery (check the README for the exact tag in your pinned version).
  4. Wire exactly one endpoint. Typical shape:
// routes/api.php
Route::get('/posts', [PostController::class, 'index']);

// PostController
public function index(Request $request)
{
    $query = Post::query();

    // Entry point — confirm the exact class/method name in the README
    // for the version you pinned; this is the one API you must get right.
    $query = Eloquery::apply($query, $request->query());

    return PostResource::collection($query->paginate());
}

Target request:

GET /posts?filter[status]=published
         &filter[author.id]=3
         &sort=-created_at
         &include=author,tags
         &paginate=25
         &search=laravel

First use case: one read-only index endpoint on a model with one or two relations. Do not roll this out across the whole API before you've seen how it handles a relation filter and an include — those are the two places this class of package always has sharp edges.

Implementation Patterns

Centralize the call in a base controller or a form request. Every endpoint should go through the same one-liner so there's a single place to add auth checks and parameter clamping later:

abstract class ApiController extends Controller
{
    protected function query(string $model, Request $request): Builder
    {
        return Eloquery::apply($model::query(), $request->query());
    }
}

Whitelist, don't trust. The config's per-model allowlists are the security boundary. Enumerate permitted filters, sorts and includes explicitly per model. If the package falls back to "anything goes" when a model has no entry, add the entry. This is the single highest-value configuration work you'll do with it.

Filter on relations with dot notation, but verify the join strategy. filter[author.id]=3 typically becomes a whereHas or a join depending on the implementation. Confirm which, because it changes your indexes: whereHas on a large table without an index on the foreign key will be the slowest thing in your API.

Keep scopes and authorization in front of the package, not behind it.

$query = Post::query()->where('tenant_id', $request->user()->tenant_id);

$query = Eloquery::apply($query, $request->query());

// Includes are a data-exposure surface — gate them.
$this->authorize('viewAny', Post::class);

Add custom filters as classes if the package supports a filter registry. Anything non-trivial (filter[created_after]=2024-01-01, a filter[status][in][]=... array) belongs in a dedicated class rather than inline controller code, so it's unit-testable in isolation.

Test through HTTP, not through the builder. The contract is the query string, so test the query string:

$this->getJson('/api/posts?filter[status]=draft&sort=-id')
     ->assertOk()
     ->assertJsonPath('data.0.status', 'draft');

Include a negative test for every allowlist: ?filter[password]=x must be ignored, not 500.

Pagination defaults. Set the default page size in config and clamp the client-supplied paginate value at the controller boundary — do not rely on the package to enforce a maximum.

Gotchas and Tips

  • Archived means no security patches and no new Laravel support. Pin the version and put an owner on it. If you're on L11+, budget a day to either fork it or port the endpoints to spatie/laravel-query-builder — the query-string grammar is close enough that the migration is mostly mechanical.
  • Unbounded include is a DoS vector. ?include=posts.comments.author.posts.comments will happily produce a query storm. Restrict includes to a whitelist and cap the depth in config.
  • Ambiguous column errors on sorts. sort=-created_at against a query that joins posts and authors produces Column 'created_at' in order clause is ambiguous. Qualify sortable columns as posts.created_at in the allowlist, or the package's sort implementation will fail only on the endpoints that use joins — i.e. in production, not in your tests.
  • Sort direction is client input. Ensure the implementation maps - to desc and anything else to asc, rather than interpolating the raw value. If it interpolates, you have SQL injection through orderByRaw. Grep the source for orderByRaw and DB::raw before shipping.
  • Pagination metadata. Decide up front whether you're returning Laravel's paginate() envelope (data + meta) or a custom resource envelope. Retrofitting this later is a breaking change for every client.
  • Search is driver-dependent. Full-text search behaves differently on MySQL vs Postgres vs SQLite. If the package uses LIKE '%term%', it cannot use an index — fine at 10k rows, a problem at 1M. Plan to replace search with Scout or Postgres tsvector when you outgrow it.
  • Debugging: dump the final SQL with ->toSql() and ->getBindings() right after the package call, not after paginate() (which rewrites the query for the count). For the count query specifically, DB::listen() is easier than trying to reconstruct it.
  • Extension points: the config's parameter names are the cheapest customization — rename filter to where to match an existing API contract rather than changing your frontend. Beyond that, custom filter classes (if present) are the intended extension point; subclassing the builder is a last resort because it will not survive a fork.
  • Don't let it own validation. A package that turns arbitrary query strings into SQL should sit behind a request validator that rejects unknown top-level keys, so malformed input fails loudly with a 422 instead of silently producing a full table scan.
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.
novasa/laravel-language-center
opencage/geocode
adolfoholzer/parameters
livenetworks/ln-starter
shitoudev/permissionmanager
johannesclimacus/laravel-tui
hocvt/seotools
vitalybaev/sypexgeo
amphibee/themosis-ignition
gito-me/fast-excel
frankyso/laravel-ipaymu
geolize-studio/laravel-chile
boomerangui/chat
jinas/dhivatar
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons