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

daveismyname/laravel-filters

A lightweight Laravel package for adding reusable, request-driven filters to Eloquent queries. Define filters once and apply them from query strings to keep controllers clean and search/filter logic organized.

View on GitHub
Deep Wiki
Context7

Getting Started

This release (v2.0.3) is a maintenance drop: dependency bumps plus Laravel 13 support. There are no breaking changes, no new filter API, and no deprecations — upgrading from v2.0.2 is a composer update dcblogdev/laravel-filters and a test run.

Minimal setup:

composer require dcblogdev/laravel-filters

The service provider is auto-discovered. If the package ships a publishable config, publish it and read it before writing any filter classes:

php artisan vendor:publish --tag=filters-config

Where to look first, in order:

  1. vendor/dcblogdev/laravel-filters/src/ — the base filter class and the model trait/scope are the whole public surface; read them rather than the README, they are small.
  2. The composer.json constraints — with v2.0.3 the package now resolves on Laravel 13, so confirm your app's laravel/framework constraint allows the combination you actually installed (the bump also pulled symfony/http-foundation to 6.4.29).
  3. Your first filter class for one index endpoint.

First use case — a paginated listing with a status filter and a text search:

// app/Filters/PostFilters.php
namespace App\Filters;

use Dcblogdev\Filters\Filters;

class PostFilters extends Filters
{
    protected array $filters = ['status', 'search'];

    public function status($value)
    {
        return $this->builder->where('status', $value);
    }

    public function search($value)
    {
        return $this->builder->where('title', 'like', "%{$value}%");
    }
}
// Controller
public function index(Request $request)
{
    return Post::filter(new PostFilters($request))
        ->with('author')
        ->paginate(25)
        ->withQueryString();
}

Implementation Patterns

One filter class per model, one whitelist per class. The $filters array is the contract: only listed keys are ever applied, so query-string input can never reach an unlisted column. Treat it as a security boundary and keep it in the class, not in the controller.

Always return the builder. Filter methods must return $this->builder (or the result of a where/whereHas call on it). Calling ->get() or ->first() inside a filter method breaks chaining, pagination, and eager loading downstream — it is the single most common way these classes rot.

Scope authorization before filtering. The filter layer does no policy checks. For anything user-facing, start from an authorized builder and hand that to the filter, or apply ->where('user_id', auth()->id()) first:

$query = Post::where('user_id', auth()->id());
return $query->filter(new PostFilters($request))->paginate();

Ranges, dates, and relations belong in closures or whereHas. A conventional pattern for a date_from/date_to pair keeps the parsing in the filter class so controllers stay thin:

public function dateFrom($value)
{
    return $this->builder->whereDate('created_at', '>=', Carbon::parse($value));
}

public function tag($value)
{
    return $this->builder->whereHas('tags', fn ($q) => $q->whereIn('slug', (array) $value));
}

Sorting is a separate concern — whitelist it explicitly. A ?sort=-created_at parameter is tempting to implement as a filter, but it needs its own column allowlist; never pass raw input to orderBy:

public function sort($value)
{
    $column = ltrim($value, '-');
    abort_unless(in_array($column, ['created_at', 'title', 'status'], true), 422);

    return $this->builder->orderBy($column, str_starts_with($value, '-') ? 'desc' : 'asc');
}

Keep eager loading out of filters. Loads (with, withCount) belong at the call site so each endpoint can differ; filters that force with() cause N+1 fixes to be duplicated across classes.

Testing. Filters are pure builder transformations, so a feature test through the route covers both parsing and SQL. Assert on the JSON payload rather than SQL strings, and add one test per whitelist entry plus one for an unlisted parameter being ignored:

$this->getJson('/posts?status=draft&bogus=1')
    ->assertOk()
    ->assertJsonPath('data.0.status', 'draft');

API resources and FormRequests compose cleanly. Validate what you support in a FormRequest (so bad input is a 422 rather than a silent no-op), then pass the same request into the filter. The two layers are complementary: validation rejects, filters select.

Gotchas and Tips

  • Typos fail silently. An unrecognised query parameter is ignored by design, which is correct for security but means ?staus=active returns the unfiltered listing with a 200. If that matters, validate allowed keys in a FormRequest using Rule::in(array_keys(...)).
  • Every value arrives as a string. Coerce explicitly: filter_var($value, FILTER_VALIDATE_BOOLEAN) for booleans, Carbon::parse() for dates, (int) for IDs. where('published', 'false') on a boolean column is a classic silent-miss bug.
  • Empty strings still trigger the filter. ?status= will run where('status', ''). Guard with if (blank($value)) { return $this->builder; } at the top of any filter that can be sent empty by a form.
  • Array parameters need guarding. ?tag[]=a&tag[]=b gives you an array; ?tag=a gives you a string. Use (array) $value plus whereIn, and be aware of URL length limits on large selections.
  • Paginator links lose filters without withQueryString(). This is a Laravel behaviour, not a package one, but it is the most-reported "the filter stops working on page 2" issue.
  • Naming is convention-based. Filter methods map to query keys by name; if you rename a method, the corresponding query parameter silently stops working. Grep for the key in tests and front-end code when renaming.
  • Don't mix builder mutation with tap. Chained ->filter() calls each receive the builder; returning $this from a method instead of $this->builder will produce confusing "method not found" errors on the model.
  • Debugging. ->dd() / ->dump() on the builder at the call site, or a DB::listen() in a service provider, shows the final SQL and bindings faster than stepping through the filter loop. To confirm a filter method was even invoked, drop a logger()->debug(__METHOD__) at the top temporarily.
  • Extension points. Register a baseline filter (tenant scoping, soft-delete visibility) by subclassing your own BaseFilters and extending $filters in each model's class — cheaper than repeating the same where in every filter method. Global defaults that apply to every query belong in a model global scope, not in a filter class.
  • Laravel 13 note. v2.0.3 is the first release that supports Laravel 13. If you are on Laravel 10–12, nothing changes functionally, but pin dcblogdev/laravel-filters:^2.0 rather than a patch version so future dependency bumps land without a manual edit. If your app still requires symfony/http-foundation 6.4.18 exactly, the bump to 6.4.29 in this release may force a transitive resolution — check composer why symfony/http-foundation before upgrading.
  • Dependabot-only changes. With no source changes in the package, a failing test after upgrade almost always means your app's dependency graph moved, not the filter behaviour. Diff composer.lock before debugging the package.
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.
jinas/dhivatar
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
cgv/localized-resource