kblais/query-filter
A Laravel package for building reusable, request-driven query filters for Eloquent models. Map parameters to filter classes, keep controllers clean, and compose testable filtering logic without cluttering your queries.
This release (v3.4.0) is a compatibility-only bump — the sole change is Laravel 13 support via PR #25. There are no new APIs, deprecations, or breaking changes, so if you are already on v3.3.x the upgrade is a composer update away. The meaningful takeaway is that the package remains actively maintained across Laravel majors, which matters when planning a framework upgrade this cycle.
First steps:
composer require kblais/query-filter (or bump to ^3.4 if already installed).if ($request->has(...)) blocks. That is your first use case.If you are on Laravel 13 already, verify the package resolves cleanly (composer why-not kblais/query-filter) before adopting; if you are on 10/11/12, this release simply removes a future blocker.
Extract, then compose. The core pattern is moving request-to-query logic out of controllers into dedicated filter classes, one per resource. Controllers shrink to something like "build query, apply filters, return resource collection." Start with one endpoint and resist the urge to build a shared base filter hierarchy on day one.
Request-key-to-method mapping is the contract. Most usage revolves around a filter class exposing one method per allowed query parameter. Treat that mapping as a public API: once the frontend starts sending ?status=active&sort=-created_at, renaming the method is a breaking change for consumers. Keep a short docblock or a test asserting the accepted keys.
Whitelist by default, not by exception. Only implement methods for parameters you intend to support. Anything not implemented should be ignored rather than passed through to the query builder. This is your primary defence against users probing column names.
Pair with FormRequests for validation. The filter decides how to query; the FormRequest decides whether the input is legal. Validating status against an enum and sort against an allowed column list keeps the filter class free of defensive checks and gives you clean 422s instead of silent no-ops or SQL errors.
Scopes for the reusable bits. Where a filter method expresses something domain-meaningful ("only published", "visible to user"), put the logic in a local Eloquent scope and have the filter call it. The filter then stays a thin translation layer, and the same constraint can be reused in jobs, commands, and policies.
Compose filters for nested resources. For endpoints like /projects/{project}/tasks, either scope the base query first and apply filters second, or apply a parent filter that constrains by relation. Always constrain by the route-bound parent before user-supplied filters so a filter cannot widen the result set beyond the authorised scope.
Test at the HTTP layer. Feature tests hitting the endpoint with a query string are far more valuable here than unit tests on the filter class — they assert the whole chain (validation → filter → query → serialisation) and catch the "parameter accepted but silently ignored" class of bug.
Rollout pattern for existing endpoints. Introduce the package on new endpoints first. For legacy ones, diff the generated SQL (or use DB::listen / Telescope) between old and new implementations on a sample of real request URLs before switching over.
Laravel 13 support is the only change here. Do not expect behavioural differences from v3.3.0. If something breaks after upgrading, it is almost certainly a Laravel-major interaction elsewhere in your dependency tree, not this package. Diff the lockfile.
Check transitive constraints on Laravel 13. A package declaring support is necessary but not sufficient — verify your other query/HTTP packages also allow 13 before committing to a framework upgrade. composer update --dry-run with -W will surface conflicts early.
Silent ignore is the default failure mode. Unrecognised parameters are typically dropped rather than erroring. That is good for security and bad for debugging: a typo in a frontend query string produces correct-looking but unfiltered results. Add an assertion in your feature tests that each supported parameter actually changes the response.
Sorting is the usual injection vector. If sort parameters map to orderBy with raw column names, you have a SQL injection or at least an information-disclosure risk (ordering by a hidden column leaks its ordering). Always map through an explicit allowlist; never pass $request->sort straight through.
Pagination and filter interaction. Filters must be applied before paginate(), and any filter that changes the result count will change page boundaries. If the frontend caches pages, version your filter contract or you will get stale, inconsistent pagination.
Eager loading is not filtering. A filter on a relation's column without whereHas will produce duplicates or silently wrong counts. Watch for filters that reach across relations and confirm they use whereHas/whereRelation rather than a join that bypasses your model's global scopes.
Global scopes and soft deletes. Filters applied on top of global scopes compose fine, but any filter that internally uses withoutGlobalScopes or raw joins can accidentally surface soft-deleted or tenancy-leaking rows. Audit filter classes for these calls.
Debugging approach. Wrap the endpoint in a temporary DB::listen or use Telescope's query tab and replay candidate query strings. Comparing the SQL for ?status=active versus no parameter is the fastest way to confirm a filter is wired up.
Extension points. The base filter class is the natural place for cross-cutting helpers (date-range parsing, boolean coercion, "empty string means unset" normalisation). If you find yourself repeating the same coercion in three filter classes, push it up — but keep that base class thin and free of domain rules.
Pin the major version. Given the package's history of following Laravel majors, constrain to ^3.4 and let Dependabot surface v4 rather than floating *. Framework-compatibility bumps like this one are exactly the releases you want applied deliberately alongside the framework upgrade, not incidentally.
How can I help you explore Laravel packages today?