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

gurgentil/eloquent-filters

gurgentil/eloquent-filters adds clean, reusable filtering to Laravel Eloquent models. Define filter classes for each parameter and let the package whitelist and apply request-based query filters, keeping controllers tidy and searches safe, flexible, and easy to maintain.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and wire it up

composer require gurgentil/eloquent-filters

Add the package's Filterable trait to the model you want to expose to query-string filtering. That trait is what installs the filter() scope / method on the model — without it, calls will silently behave like a normal query or throw a BadMethodCallException, depending on version.

use Gurgentil\EloquentFilters\Filterable;

class Post extends Model
{
    use Filterable;
}

First use case: a search endpoint

The canonical use case is a listing endpoint where the front end sends ?title=foo&published=1&category=news and you want the controller to stay thin. Create one small filter class per concern, each responsible for a single where/whereHas/scope call:

namespace App\Filters;

use Gurgentil\EloquentFilters\Filter;
use Illuminate\Database\Eloquent\Builder;

class TitleFilter extends Filter
{
    public function apply(Builder $builder, $value): Builder
    {
        return $builder->where('title', 'like', "%{$value}%");
    }
}

Then use it from the controller:

$posts = Post::filter($request->only(['title', 'published', 'category']))
    ->paginate();

Where to look first

  • README.md — short, but the only real documentation for key-to-class resolution rules.
  • src/Filterable.php (the trait) — shows exactly how keys map to filter classes and what happens to unknown keys.
  • src/Filter.php + any Filter contract — the base class you extend; confirms the expected apply() signature and any constructor/container wiring.
  • Because the source is only a handful of files, reading it end-to-end takes ~10 minutes and is more reliable than guessing from examples.

Implementation Patterns

One filter class per key, registered explicitly or by convention

The package's value is that each filter is a testable unit. Two common setups:

  1. Convention-based: a query key like title resolves to App\Filters\TitleFilter. Fast to add, but the mapping is implicit — new team members have to know the namespace and suffix rules.
  2. Explicit registration: expose a $filters array or filters() method on the model mapping keys to classes. Slightly more boilerplate, but it doubles as the whitelist of accepted parameters and is what I'd recommend for anything beyond a prototype.

Whichever the package version supports, treat the mapping as the contract between your API and the model.

Keep filters dumb and composable

Each filter should do one thing and return the builder. Composition then happens naturally:

class CategoryFilter extends Filter
{
    public function apply(Builder $builder, $value): Builder
    {
        return $builder->whereHas('categories', fn ($q) => $q->where('slug', $value));
    }
}

This also means filters are trivially unit-testable: instantiate the filter, pass a Post::query() and a value, assert the SQL or the result set. Do this — it's the cheapest safety net you'll get with a 15-star package.

Validate before you filter

Filters assume well-formed input. Pair the scope with a FormRequest or $request->validate([...]) so that published is a boolean, category is a slug that exists, and dates are dates. The package will happily pass ['title' => ['array']] into your apply() and you'll get a 500 instead of a 422.

Integration tips

  • Call filter() after with()/withCount() so eager loads aren't accidentally reset by builder clones inside filters.
  • Combine with pagination and sorting as normal — the scope returns a Builder, so nothing special is needed.
  • Reuse filters across models when the column semantics match (e.g., a CreatedAfterFilter), but be deliberate: a filter that references a relationship (categories) is not portable to a model without that relation.
  • For admin panels where many optional filters are sent, loop key → class yourself in the model's filters() method rather than duplicating if ($request->has(...)) blocks in the controller.

Gotchas and Tips

Age and version compatibility

Last release January 2021 and 15 stars. Before adopting, check the composer.json constraints against your Laravel/PHP version. If you're on Laravel 10/11 with PHP 8.2+, expect to test carefully or fork. Pin it, and be prepared to vendor it (it's tiny) if a future Laravel upgrade breaks the trait's signatures.

Unknown keys and empty values

Two classic failure modes:

  • A request param that doesn't map to a filter — confirm whether the trait ignores it silently or throws. If it throws, a stray ?utm_source=... in a shared link 500s your listing page. Whitelist keys at the controller boundary ($request->only([...])) regardless.
  • Empty strings / null / 0. Decide per-filter whether to skip early (if (blank($value)) return $builder;) or apply. Sending ?title= from a cleared search box should not produce WHERE title LIKE '%%' if that changes query plans — and never WHERE title = ''.

SQL injection and LIKE safety

The filter classes receive raw request values, so interpolation is your responsibility. "%{$value}%" is fine for PDO-bound parameters, but don't build column names, orderByRaw, or whereRaw from user input. Escape % and _ in LIKE terms if you care about literal matching.

Debugging

Because filtering is spread across many small classes, a wrong result can be hard to trace. Two cheap techniques: ->dd() / ->toSql() on the final builder to see which constraints landed, and a temporary Log::info(class_basename(static::class), [$value]) in the base filter's apply(). If the trait applies filters in an unexpected order, log there too — order rarely matters for WHERE clauses but very much does for orWhere combinations.

Extension points

The base Filter class is the intended extension seam: put cross-cutting behaviour (value normalisation, logging, "skip if blank", tenant scoping) in an App\Filters\BaseFilter and extend it everywhere. That's also where you'd add a TenantFilter that every model registers, so multi-tenancy isn't quietly forgotten on a new endpoint.

Consider alternatives if scope grows

If you find yourself needing nested includes, sparse fieldsets, or sorting/?sort=-created_at semantics, look at spatie/laravel-query-builder — it's actively maintained and has a much larger community. gurgentil/eloquent-filters is a good fit when you want a small, readable filter layer and are comfortable owning it. Treat it as a starting point, not infrastructure you can afford to leave unmaintained.

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.
diephp/sequences
users-au/socialite-provider
twidpay/twid-logger
mrnewport/laravel-plaid
mrnewport/laravel-repo
mrnewport/laravel-docsign
mrnewport/laravel-stow
mrnewport/laravel-flow
mrnewport/laravel-priceable
mrnewport/laravel-nestedterms
fredmeng/laravel-apidoc-generator
itstructure/laravel-multilingual-tools
pdustdar/elasticsearch
markupai/php-sdk
rlacerda83/lumem-microservice-shipment
uni-method/symfony-lib
tacticmedia/scim-server-bundle
neika-ai/symfony-bundle
mattleads/agent-publishing-bundle
glavweb/framework-bundle