eloquent-filter/filter
Eloquent Filter adds request-driven filtering to Laravel Eloquent models. Define allowed filters and custom filter classes, then filter queries through query-string parameters, keeping APIs, admin panels, and search endpoints clean, composable, and easy to maintain.
Install the package and put the trait on the models you want to filter:
composer require mehdi-fathi/eloquent-filter
use EloquentFilter\Filterable;
class Post extends Model
{
use Filterable;
public function getTitleFilter(string $title)
{
return $this->where('title', 'like', "%{$title}%");
}
public function getStatusFilter(string $status)
{
return $this->where('status', $status);
}
}
The contract is simple: query-string keys map to methods named get{StudlyKey}Filter. ?status=published calls getStatusFilter('published'). Whatever the method returns is chained onto the underlying Eloquent builder, so it behaves exactly like a local scope.
Version note: as of 4.5.6 the package explicitly supports Laravel 13.x (in addition to earlier supported versions). If you are planning an upgrade, this is the minimum version to pin — older releases will not declare compatibility with Laravel 13 and may resolve against an untested framework.
First use case — a filterable index endpoint with no bespoke query-building code in the controller:
public function index(Request $request)
{
return Post::filter($request->only(['title', 'status']))
->with('author')
->paginate(20);
}
Post::filter() with no argument reads the current request; passing an explicit array is the safer default in controllers, jobs, and tests.
Where to look first, in order: the README for the filter-naming convention, src/ for the trait that resolves filter names and applies them, and the package's own tests for the exact whitelist/ignore API your pinned version exposes. Because the package is small and has few dependents, treat the source as the specification.
Treat filter methods as scopes with request-driven arguments. They should do one thing and return a query constraint. Anything more elaborate belongs in a service or a query object.
Centralize filters in a trait when several models share the same filterable columns (created_at, status, search), then use it alongside Filterable. This keeps models from growing a dozen near-duplicate methods.
Whitelist aggressively. Define an allow-list on the model (typically a $filterable property or a filterable() method, depending on version) so only declared filters can ever be invoked from the outside. This is your primary defense against a caller probing for an unexpected get*Filter method.
Exclude infrastructure params. Pagination, sorting, and includes are not filters. Register them via the package's ignoreRequest() hook so ?page=2 and ?sort=title never get treated as filter candidates.
Normalize inside the filter. Coerce booleans, trim strings, map enum-ish values:
public function getPublishedFilter($value)
{
return $this->where('published_at', filter_var($value, FILTER_VALIDATE_BOOLEAN) ? '!=' : '=', null);
}
Guard optional filters with filled() so an empty ?title= doesn't force a LIKE '%%' scan or an unwanted WHERE.
Compose with the rest of the builder. Because filters return the builder, you can interleave with(), when(), and ordering around filter() freely.
Relation filters use dotted keys (?author.name=ali) mapped to relation-aware filter methods; keep them explicit and eager-load the relation to avoid N+1.
Testing: skip HTTP entirely and pass arrays — Post::filter(['status' => 'draft'])->toSql() — which makes filter tests fast and deterministic and avoids the no-request failure mode.
Controller pattern: keep the controller at one line (Model::filter($request->only(...))), and let the model own the semantics. This is the package's whole value proposition: listing endpoints become declarative.
Framework-upgrade pattern: if you are moving to Laravel 13, bump this package to ^4.5.6 in the same PR as the framework constraint change, then run your filter test suite against the new major. Because filters are plain builder calls, the realistic breakage surface is small — mainly deprecations in the query builder and any change to request handling — but do not bundle the two upgrades without a green test run.
user_name in the query string becomes getUserNameFilter, not getUser_nameFilter. If a filter "isn't firing," dump the compiled query and verify the method name spelling and casing first.''. Always short-circuit with filled() unless you genuinely want the empty case.filter() without arguments depends on a live request. In queued jobs, Artisan commands, and some test contexts there is no request binding, so the call becomes a no-op or an error. Prefer passing the array explicitly.whereRaw. Bind parameters; the package passes your value through untouched, so injection safety is entirely your responsibility inside the filter method.$this->where(...)). Returning something else — a boolean, a collection, null from a conditional branch — breaks the chain in ways that are hard to trace. Always return a query builder on every path.?author.name= filter and an author column on the parent table are different things; be explicit about which one you mean.->toSql() / ->getBindings() after filter([...]) to see exactly which filters ran. Log the input array in the controller when a filter "disappears" in production but works locally.composer.json, read the changelog between releases (4.5.6 added Laravel 13.x support; the 2026-02-27 release is recent), and wrap the public surface (filter(), whitelist, ignore hook) behind your own base trait so a breaking rename is a one-file fix.composer update refuses to move you to Laravel 13, check that this package is at ^4.5.6 before hunting for a problem in your own code — the framework constraint in an older release is the usual culprit.FilterableModel base class (or shared trait) that declares the whitelist, ignoreRequest(), and any cross-cutting filters. Everything else stays as plain scope-like methods on individual models.How can I help you explore Laravel packages today?