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 Eloquent Model Filter Laravel Package

leocarmo/laravel-eloquent-model-filter

A Laravel package for adding declarative filters to Eloquent models. Define allowed filters once, apply them from request query strings, and keep complex query logic reusable, readable, and easy to test across your application.

View on GitHub
Deep Wiki
Context7

Getting Started

Confirm it will install before you plan around it. This package's last release was August 2018 and it declares constraints for the Laravel versions of that era (5.5–5.7). On Laravel 9/10/11 composer require will usually fail on illuminate/support. Decide up front whether you will fork it, bump the constraints in your own fork, or vendor the handful of source files into app/Support/. Given ~11 stars and zero dependents, assume you own the maintenance.

composer require leocarmo/laravel-eloquent-model-filter

Where to look first: open src/ and read the actual trait and the base filter class before writing any code. The README is thin and the naming (trait namespace, the property that declares allowed keys, the scope name) is the part you must not guess. Read composer.json at the same time for the PHP/Laravel constraints.

Minimal wiring (verify exact names against src/ — the shape below is the pattern, not a guarantee):

use Leocarmo\LaravelEloquentModelFilter\Traits\Filterable;

class Post extends Model
{
    use Filterable;

    // whitelist of query-string keys this model will honor
    protected $filterable = ['title', 'status', 'created_at'];
}
public function index(Request $request)
{
    $filters = $request->validate([
        'title'   => ['nullable', 'string', 'max:255'],
        'status'  => ['nullable', 'in:draft,published'],
    ]);

    return Post::query()
        ->filter($filters)
        ->paginate(20);
}

First use case: an admin/API index endpoint with a filter bar (text search on a title, a status dropdown, a date range). That is the sweet spot — a small, declarative whitelist instead of a growing pile of ->when($request->filled(...)) in every controller method.

Implementation Patterns

Whitelist per model, never per request. The filter definition belongs next to the model (or in a dedicated filter class if the package supports one), not derived from $request->all(). Treat the whitelist as part of the model's public query contract and review changes to it.

Controller shape: validate → filter → eager load → paginate. Keep the filter call adjacent to the query it modifies, and keep with() explicit so a filter change never silently triggers lazy loading.

$query = Post::query()
    ->with(['author:id,name', 'category:id,name'])
    ->filter($validated);

if ($request->filled('sort')) {
    $query->orderBy(
        in_array($request->sort, ['title', 'created_at']) ? $request->sort : 'created_at',
        $request->direction === 'asc' ? 'asc' : 'desc'
    );
}

return PostResource::collection($query->paginate(20));

Sorting is usually not covered by the package — keep a separate, equally strict whitelist for orderBy columns.

Extract a filter class once a model passes ~5 keys. If the package exposes a base filter class, subclass it and override the resolution method for the awkward fields (date ranges, LIKE with escaped wildcards, enum mapping). This is the extension point that keeps model classes clean and makes the logic unit-testable without HTTP.

Compose with whereHas for relation filters. A filter on author.name should become whereHas('author', fn ($q) => $q->where('name', ...)), not a join plus a bare name predicate. If the package can't express that, add it in a custom filter method rather than joining in the controller.

Mirror it with Form Requests. Put the validation rules in a PostIndexRequest so the whitelist in the model and the validation rules stay visibly paired; the reviewer can diff both in one place.

Test at both levels. Feature tests hitting the endpoint with query strings (?status=draft&title=foo) for the wiring, plus unit tests on the filter class for the per-field logic — the latter is where the interesting bugs live and it doesn't need a database.

Reuse across endpoints. A shared filter definition means the same query-string semantics work for the admin table, an export job, and a public API. That consistency is the main payoff for taking the dependency over hand-rolling when() clauses.

Gotchas and Tips

  • Validation is entirely your job. The package filters; it does not authorize or sanitize. Anything reaching filter() should already be validated, and any key not in the whitelist should either be dropped upstream or confirmed to be ignored safely (check the source — a strict implementation may throw on unknown keys).
  • Column-name ambiguity on joins. Filters like name become ambiguous the moment you join. Qualify with the table (posts.status) in the filter mapping, or filter via whereHas instead.
  • LIKE semantics differ by driver. Postgres is case-sensitive for LIKE (you want ILIKE/lower()), MySQL depends on collation, and user-supplied %/_ in a search box are wildcards unless escaped. Verify what the package does with the value before assuming a literal substring match.
  • Empty string vs null vs "0". A filter bar submits empty inputs. Confirm whether ?status= is ignored or becomes WHERE status = ''. Normalize with array_filter(..., fn ($v) => $v !== null && $v !== '') before passing filters in.
  • Never let a request value become a column name or raw SQL. If you extend the filter class, keep column names hard-coded in your mapping and pass values only as bindings. No whereRaw with interpolated request data.
  • Debug by dumping the builder, not by guessing. ->filter($filters)->toSql() plus getBindings() shows exactly what was appended; a surprising empty result is almost always a filter you didn't realize was applied.
  • Index what you filter. Low-cardinality columns (status) combined with orderBy created_at plus paginate() is the classic slow query. Check with EXPLAIN before shipping the filter bar.
  • Beware global scopes and soft deletes. Filters compose with whatever the model already applies; a filter on deleted_at or a tenant column can interact badly with existing global scopes. Test with the model's real booted() state in place.
  • Don't edit vendor/. If you need a behavior change, fork the repo (it's small), override the filter class, or bind a custom implementation in the container. Anything patched in vendor/ disappears on the next composer update.
  • Pin the version and note the risk in the PR description. With a 2018 last release and no dependents, the honest expectation is that you will eventually replace this with ~50 lines of your own scopes or with a maintained alternative. Keep the filter calls behind a small interface (or at least confined to a few models) so that swap is a mechanical change rather than a refactor.
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