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

Laravel Filters Laravel Package

mykeels/laravel-filters

A lightweight Laravel package for declarative, request-driven Eloquent filtering. Define which filters a model allows and let query-string parameters like ?status=active narrow results safely, keeping controllers clean and queries reusable, with support for custom filter logic.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and confirm the shape of the API first. The package is small (16 stars), so the README is the source of truth — read it before writing any filter classes, and check the src/ directory to see the actual base class/interface you need to extend.

composer require mykeels/laravel-filters

Then verify:

  1. Service provider discovery — if the package ships one, Laravel 5.5+ auto-discovers it. Run php artisan package:discover and confirm it appears in bootstrap/cache/packages.php.
  2. Publishable config — try php artisan vendor:publish --provider="Mykeels\Filters\FilterServiceProvider". If nothing publishes, there is no config to worry about and you can skip this step entirely.
  3. The extension point — open the package's base Filter class (or interface) and note the exact method signature. Most packages in this family use either an apply($query, $value) method or a filter($value) method; getting this wrong is the #1 source of "why is nothing happening" confusion. Read it before writing your first class.

Where to look first: the model/query-builder binding. Find whether filtering is exposed as a local scope (Model::filter($request->query())), a trait (,Filterable), or a middleware that mutates the request. That single decision drives everything else.

First use case — a list endpoint with a search box and two facets:

GET /api/users?name=ada&status=active&created_after=2024-01-01

Create one filter class per concern rather than one giant UserFilter. Start with the simplest possible filter to prove the wiring end-to-end:

namespace App\Filters;

use Mykeels\Filters\Filter; // confirm this namespace against the repo

class NameFilter extends Filter
{
    public function apply($query, $value)
    {
        return $query->where('name', 'like', "%{$value}%");
    }
}

Once ?name=ada returns a narrowed result set, you know the plumbing works and you can add filters confidently.

Implementation Patterns

One filter per column / one filter per concept

The composability value of this package comes from keeping each filter atomic. A StatusFilter should only know about status; a DateRangeFilter should only know about dates. Compose them in the mapping rather than branching inside a single class.

// Typically: a $filters map on the model, or passed to the scope
protected $filters = [
    'name'          => \App\Filters\NameFilter::class,
    'status'        => \App\Filters\StatusFilter::class,
    'created_after' => \App\Filters\CreatedAfterFilter::class,
];

The map doubles as your whitelist. Anything not in the map is silently ignored, which is exactly what you want — a client sending ?password_hash=x or ?is_admin=1 must never reach the query builder.

Controller as the composition point

Keep controllers thin but explicit about which query params are legal for which endpoint:

public function index(Request $request)
{
    $users = User::query()
        ->filter($request->only(['name', 'status', 'created_after']))
        ->with('team')
        ->paginate(25)
        ->withQueryString();

    return UserResource::collection($users);
}

$request->only([...]) at the call site makes the endpoint's contract readable in one glance and keeps the same model reusable across endpoints with different filter sets (public listing vs. admin console).

Composite / multi-value filters

For ?status=active,pending or ?ids[]=1&ids[]=2, normalise inside the filter, not in the controller. This keeps the "how do I split this string" decision in one place:

class StatusFilter extends Filter
{
    public function apply($query, $value)
    {
        $values = is_array($value) ? $value : explode(',', $value);

        return $query->whereIn('status', array_filter($values));
    }
}

Scoping and relationship filters

Compose with relationship constraints where the filter semantically belongs to a relation. This is where a filter package earns its keep on real apps — ?team=engineering should become a whereHas, not a join you hand-write every time.

class TeamFilter extends Filter
{
    public function apply($query, $value)
    {
        return $query->whereHas('team', fn ($q) => $q->where('slug', $value));
    }
}

Reuse outside HTTP

Because filters operate on the query builder, they are equally usable from console commands, jobs, and exports. That is the strongest argument for this package over a fat controller: User::query()->filter(['status' => 'active']) works in php artisan too, with no request coupling.

Testing

Test filters against the builder, not through HTTP — it is faster and pins down the contract:

it('filters by status', function () {
    User::factory()->create(['status' => 'active']);
    User::factory()->create(['status' => 'banned']);

    $result = User::query()->filter(['status' => 'active'])->get();

    expect($result)->toHaveCount(1)
        ->and($result->first()->status)->toBe('active');
});

Add one HTTP-level test per endpoint that asserts an unknown param is ignored — that is the regression you actually care about.

Gotchas and Tips

This package is small and lightly maintained — treat it as a pattern, not a platform. With 16 stars and a 0.49 score, expect thin docs, no changelog guarantees, and possible incompatibility with newer Laravel query-builder internals.

  • Pin the version in composer.json ("mykeels/laravel-filters": "^1.0" — check the real constraint) and commit composer.lock. If the package becomes unmaintained, you want a stable point to fork from.
  • Have an exit plan. Because each filter is a small class with one method, migrating off is cheap: copy the classes into app/Filters, drop the base-class extends, and replace the scope with ~15 lines in a trait. Decide this before you have 40 filter classes.
  • Confirm the method signature against src/, not the README. Small packages frequently drift; a README example that no longer matches the base class produces silent no-ops (the filter runs, returns nothing, and the query passes through unchanged).

Security / correctness:

  • Whitelist, never reflect. If the package auto-resolves filter class names from query-string keys, an attacker can attempt to instantiate arbitrary classes. Always pass an explicit array of allowed keys ($request->only([...])) rather than $request->all().
  • Validate values, not just keys. ?created_after=not-a-date should 422, not silently return everything or throw a 500 from the query builder. Pair filters with a FormRequest whose rules mirror the filter map.
  • Beware whereRaw. If you write a filter using raw SQL, bind parameters (whereRaw('LOWER(name) = ?', [strtolower($value)])) — never interpolate the query-string value.
  • Empty strings are a real case. ?name= arrives as '', not null. Decide per filter whether to skip (if (blank($value)) return $query;) or filter to nothing. Blanking the whole result set on an empty search box is a classic bug.

Query behaviour:

  • Eager-load inside filters carefully. A whereHas is fine; calling ->with() inside a filter silently changes hydration for the whole endpoint and can surprise callers. Keep with() in the controller.
  • like and case sensitivity differ between MySQL (case-insensitive by default collation) and PostgreSQL (case-sensitive). If you support multiple databases, use whereIlike-style handling or normalise explicitly — otherwise tests pass locally and fail in CI against Postgres.
  • Pagination links lose filters unless you call ->withQueryString() on the paginator. Easy to miss, and users notice immediately.
  • Count queries. Composite filters using whereHas on large tables can turn a listing page into a slow one; run ->toSql() and check the indexes you actually need (usually on the foreign key and the filtered column).

Debugging:

  • Dump the builder rather than guessing: dd(User::query()->filter($filters)->toSql()) and ->getBindings(). If the SQL looks right but rows are wrong, the issue is in a filter's apply(), not the package.
  • If a filter never fires, check three things in order: (1) the key is present in the whitelist map, (2) the value isn't being stripped as blank, (3) the class is resolvable (fully-qualified namespace, composer dump-autoload after adding files).
  • Config caching: if you did publish a config file, remember php artisan config:cache during deploys — a stale cache silently ignores new filter registrations.

Extension points worth knowing:

  • Abstract base filter — if you find yourself repeating if (blank($value)) return $query;, add your own App\Filters\Filter that extends the package's base and handles the no-op case centrally. Everything else extends yours, and migrating off the package later is a one-file change.
  • Global filters — auth scoping (tenant, visibility, soft-deleted handling) belongs in a scope or middleware, not in a query-string filter. Filters should narrow an already-authorised query; they must never be the thing that keeps users from seeing other tenants' data.
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.
mehedimi/laravel-amp-mail
happones/laravel-vue-i18n-generator
jwerd/inertia-table
siberfx/flysystem-wasabi
hollyit/token-replace
ippanelcom/laravel-sdk
adachsoft/changelog-linter
adachsoft/collection
payerurl/telegrambot-crypto-payment-instant-payout
dskripchenko/laravel-hybrid-search
riorizkyrainey/lara-ced
mehedimi/static-vel
zharp/zharp
nonsapiens/bigquery-model-sync
lgu-bayambang-ict-office/lgu-laravel-development-standards
andydefer/laravel-support
se7enxweb/flysystem
lampwick/laravel
jeslage/statamic-emojis
micahdshackelford/rmap-laravel