# eloquent-builder moves Laravel request filters out of the controller

> eloquent-builder is an MIT-licensed PHP package that maps incoming request parameters to reusable Eloquent filter classes or inline quick filters, so a controller stops growing an if statement per query parameter. The current release is v6, it needs PHP 8.3 and Laravel Eloquent, and Lumen is no longer supported.

**mohammad-fouladgar/eloquent-builder** — Advanced, request-driven filtering for Laravel Eloquent models.

- Repository: https://github.com/mohammad-fouladgar/eloquent-builder
- Website: https://medium.com/@mohammadfouladgarphp/laravel-eloquent-filter-all-in-one-d903ededbd19
- Stars: 525 · Forks: 42
- Language: PHP
- License: MIT
- Published: 2026-10-22 · Updated: 2026-10-22 · Language: en
- Canonical page: https://hysenlabs.com/projects/mohammad-fouladgar-eloquent-builder

## The problem: one condition per request parameter

The README opens with the situation the package exists to fix. Filtering an Eloquent query gets hard to maintain when every request parameter needs its own conditional logic, and the example is the shape of controller code most Laravel applications accumulate:

```php
$users = User::where('is_active', true);

if ($request->has('age_more_than')) {
    $users->where('age', '>', $request->age_more_than);
}

if ($request->has('gender')) {
    $users->where('gender', $request->gender);
}

if ($request->has('has_published_post')) {
    $users->whereHas('posts', function ($query) use ($request) {
        $query->where('is_published', $request->has_published_post);
    });
}
```

The third branch is the instructive one, because a whereHas over a relation is not something you can express as a simple where, and the conditional wrapping it is where the controller starts to look like a query builder with extra steps. The package's answer is to move each of those branches into its own class and let the request keys resolve to those classes by name.

## What the controller looks like afterwards

The replacement is three chained calls. You name the model, hand over the request filter array, apply the resolved filters and fetch:

```php
return EloquentBuilder::model(User::class)
    ->filters($request->filter)
    ->thenApply()
    ->get();
```

The controller's job is now application flow, and the query-specific logic lives inside the filter classes. The name resolution is the part that makes it work, since the request key age_more_than is resolved to AgeMoreThanFilter. That mapping is conventional rather than magical, and it is the same convention Laravel uses for form requests and policies, so a Laravel developer reads it without friction. The README also notes that the same key can be pushed programmatically rather than read from the request, and that in v6 when the same filter key is pushed more than once the latest value takes precedence, which is a small behavioural change from earlier versions.

## Defining a filter class and generating it

A filter extends the package's Filter contract, Fouladgar\EloquentBuilder\Support\Foundation\Contracts\Filter, and must implement apply(). The canonical example:

```php
<?php

namespace App\EloquentFilters\User;

use Fouladgar\EloquentBuilder\Support\Foundation\Contracts\Filter;
use Illuminate\Database\Eloquent\Builder;

class AgeMoreThanFilter extends Filter
{
    public function apply(Builder $builder, mixed $value): Builder
    {
        return $builder->where('age', '>', $value);
    }
}
```

The class name uses the Filter suffix, and with the default namespace the filters for User live under App\EloquentFilters\User, so a directory tree holds AgeMoreThanFilter.php and GenderFilter.php side by side. You do not have to write these by hand, because there is an Artisan command for it:

```bash
php artisan eloquent-builder:make user age_more_than
```

and it takes several names at once, for example php artisan eloquent-builder:make user age_more_than gender. The README also mentions that Laravel local scopes can be used inside filter classes, which is the escape hatch for a condition that already has a scope somewhere in your model.

## Quick Filters, and the precedence rules that come with them

For simple conditions you can skip the class entirely, and the README lists a set of quick filter kinds: Exact, Partial, Scope, Callback, Trashed, Includes and Fields, with a section on quick filter precedence and another on filter groups. The practical value of quick filters is that a request parameter like a name search can be handled without a class, while a condition that needs a relation still gets a proper filter class. There is also a fluent macro form that works directly on a model or query, User::filter(request()->filter)->get(), which is the simplest way to apply standard filters without going through EloquentBuilder::model(). The README is explicit that the full EloquentBuilder API is what you need for the advanced features, naming quickFilters(), filterGroups(), defaults() and ignoreValues() as the things the macro does not cover, so the choice is between the two levels of the library rather than a single way in.

## The request shape the package expects

The recommended request shape puts the filter parameters under a single filter key, and the README shows it as a URL query string:

```http
GET /api/users?filter[age_more_than]=25&filter[gender]=male
```

The tip is repeated in the quick start, which tells you it matters. A single namespaced key is what lets the package distinguish a filter parameter from an ordinary query parameter, and it keeps the controller signature honest, since one request key carries the whole filter set rather than a flat list of names that might collide with pagination or sorting parameters. The example is also an API shape, which matches where this problem actually appears, since the kind of request that carries a dozen optional filters is an API consumer's request.

## Filters that do not exist, and empty values

Two behaviours decide what happens when a request is not well formed, and both are documented rather than left to chance. There is a missing filter behaviour section, which covers what happens when a request names a filter the package cannot resolve, and a separate section on ignoring empty and null values, with an ignoreValues() method and a defaults() method for supplying fallback values. The security section in the table of contents points at a related concern, and the authorization section at the fact that a filter class is code you wrote, so an attacker who can choose the filter key can choose which class runs, which is exactly why the missing filter behaviour matters and why authorization is treated as its own topic.

## The project's own shape, and what the version history says

The repository is a Composer package with a src/ directory, tests/, a config/ directory, a database/ directory, phpunit.xml for the test suite and pint.json plus rector.php for style and automated refactoring, with a CHANGELOG.md, a VERSION file, an UPGRADE.md and a bump-version.sh script. The public release history in the repository listing is a single tag, v1.0.0 from 2019, which is not the whole story, because the README states that the current release is v6 and that v6 follows v5 with breaking changes documented in UPGRADE.md. The repository was pushed to on 2026-09-29, so the work on it is current even though the visible tag list is not. The licence is MIT, and the package is on Packagist as mohammad-fouladgar/eloquent-builder. The homepage points at a long-form Medium article about building an all-in-one Laravel Eloquent filter.

## Where eloquent-builder is the wrong choice

Three cases. The first is a small API with three fixed filters, where a query scope or a simple where does the job and the package adds a class per condition, a namespace convention and a request key format to maintain. The second is a project that has standardised on a different filtering approach, because a second mechanism in the same codebase is worse than either one alone, and the package's own documentation points at the official Laravel tooling for the cases where that is enough. The third is a Lumen application, which the README states is no longer supported, so a Lumen team has to look elsewhere. Against writing the conditionals by hand, the difference in approach is that the package moves the branch from the controller into a named class that Laravel can resolve by convention and that a test can instantiate directly, and the cost is a layer of indirection plus a decision about where filters live and what a missing filter should do.

## Conclusion

eloquent-builder is a good fit for an API-heavy Laravel application where the same query parameters are repeated across a dozen endpoints and each one has grown its own conditional block, because the package turns that duplication into named filter classes that are unit testable. It is a poor fit if your filtering needs are three or four static conditions, where a query scope does the same job with less machinery, and it is a poor fit for a Lumen project, which the project states is no longer supported. Check three things before you adopt it: that you are on PHP 8.3 or newer with Laravel Eloquent, that you read UPGRADE.md if you are coming from v5, since the v6 line changed behaviour, including how a repeated filter key is resolved, and that you decide where your filter classes will live, because the default namespace convention is App\EloquentFilters followed by the model name, and the package will look for your class there unless you configure a different namespace.

## FAQ

### What does eloquent-builder do?

It lets you map incoming request parameters to reusable Eloquent filters so that controllers do not accumulate conditional query logic. The filtering logic can live in dedicated filter classes or in lightweight quick filters, with controllers staying focused on application flow.

### How do I install eloquent-builder?

With Composer, using composer require mohammad-fouladgar/eloquent-builder. The requirements are PHP 8.3 or newer and Laravel Eloquent, and the README warns that the Lumen framework is no longer supported.

### How do I create a filter class for eloquent-builder?

Create a class that extends Fouladgar\EloquentBuilder\Support\Foundation\Contracts\Filter and implements apply(), using the Filter suffix, with the default namespace placing User filters under App\EloquentFilters\User. You can also generate them with php artisan eloquent-builder:make user age_more_than, passing several names at once.

### What are Quick Filters in eloquent-builder?

They are inline filters for simple conditions, so you can skip creating a filter class. The README lists Exact, Partial, Scope, Callback, Trashed, Includes and Fields, with separate sections for quick filter precedence and filter groups, plus quickFilters(), filterGroups(), defaults() and ignoreValues() as the advanced features that require the full EloquentBuilder API.

### Does eloquent-builder still support Lumen?

No. The installation requirements carry a warning that the Lumen framework is no longer supported. The current release is v6, and the README points to UPGRADE.md for the breaking changes and new features when coming from v5.

## Sources

- [License: MIT](https://github.com/mohammad-fouladgar/eloquent-builder/blob/master/LICENSE)
- [mohammad-fouladgar/eloquent-builder on GitHub](https://github.com/mohammad-fouladgar/eloquent-builder)
- [Project website](https://medium.com/@mohammadfouladgarphp/laravel-eloquent-filter-all-in-one-d903ededbd19)
- [README](https://github.com/mohammad-fouladgar/eloquent-builder/blob/master/README.md)
- [Releases](https://github.com/mohammad-fouladgar/eloquent-builder/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/mohammad-fouladgar-eloquent-builder
