velitsol/eloquent-filter
A Laravel package for adding clean, reusable filters to Eloquent models. Define filter classes and apply them via simple query strings or request parameters, keeping controllers slim and queries readable. Supports custom filter logic, sorting, and easy extension for building flexible, searchable API...
Install and confirm what you actually got. The package is archived and has zero dependents, so treat the source as the documentation:
composer require velitsol/eloquent-filter
composer show velitsol/eloquent-filter # confirm version + Laravel constraint
ls vendor/velitsol/eloquent-filter/src # read the trait/class before trusting the README
Check composer.json inside the vendor directory first — an archived package often pins an old illuminate/* range and will silently break auto-discovery on Laravel 10/11.
Find the entry point. Query-string filter packages of this vintage generally expose one of two shapes: a trait you add to a model that gives you a filter()/scopeFilter() method, or a standalone filter class you instantiate with the request. Open the main class in src/ and look for the method that accepts an array or Request.
First use case — a searchable index endpoint. Attach the trait to a model, then feed it only the query string:
// app/Models/User.php
use Velitsol\EloquentFilter\Concerns\FiltersQueryStrings; // verify namespace in src/
class User extends Model
{
use FiltersQueryStrings;
}
// app/Http/Controllers/UserController.php
public function index(Request $request)
{
return User::filter($request->query())->paginate(25);
}
Then hit /users?name=alice&status=active and confirm with ->toSql() that both constraints landed. That single request is your smoke test for every later change.
Treat the model as the contract. Don't let arbitrary query keys hit the database. Most implementations of this pattern use a $filterable array or an overridable method — define it explicitly and reject everything else:
protected array $filterable = ['name', 'email', 'status', 'created_at'];
If the package doesn't ship a whitelist, subclass or wrap it so one exists. This is the single most important integration decision.
Filter in the controller, not the model scope chain. Keep the filter application adjacent to the request so it's obvious which endpoints are query-string driven:
public function index(Request $request)
{
$query = User::query();
if ($request->hasAny(['name', 'status', 'created_at'])) {
$query->filter($request->query());
}
return $query->with('team')->paginate();
}
Relationship filtering. The package advertises relationship support, typically via dotted keys (?posts.title=hello) translating to whereHas. Verify the separator in the source before documenting it for your frontend. Pair it with with() so the eager load and the constraint use the same relation:
User::with('posts')->filter($request->query())->get();
Validate the shape, not the semantics. Put a FormRequest in front of the endpoint to bound array sizes, depth, and allowed keys. Even a permissive rule set beats raw $request->all():
public function rules(): array
{
return [
'name' => ['nullable', 'string', 'max:255'],
'status' => ['nullable', Rule::in(['active', 'invited'])],
'posts.title' => ['nullable', 'string', 'max:255'],
];
}
Compose with sorting and pagination. The filter should produce a builder you hand off, never a collection. ->filter(...)->orderBy('name')->paginate() keeps counts correct; anything that calls ->get() internally will break paginate() and total counts.
Test at the HTTP layer. Because the input is a URL, feature tests are cheaper than unit tests here:
$this->getJson('/api/users?status=active')
->assertOk()
->assertJsonPath('data.0.status', 'active');
Add a negative test (?status=;DROP TABLE) asserting a 422, not a 500.
It's archived — plan for ownership. No security patches, no Laravel compat updates. Pin the exact version in composer.json, and if you're depending on it in production, copy the source into app/Support/ and own it. A 22-star, 0-dependent package is not something to leave on ^1.0.
Unvalidated column names are an injection vector. If the implementation maps URL keys directly to where($key, ...), an attacker can probe columns or force expensive queries. Whitelist keys before the package ever sees them — this is not optional.
Use $request->query(), not $request->all(). all() merges route parameters and JSON bodies; a POST /users/5?name=x can then filter on the route id and quietly override your scoping.
Empty strings are not null. ?status= usually arrives as '' and produces where status = '' — zero rows instead of "no filter." Normalize before filtering:
$filters = array_filter($request->query(), fn ($v) => $v !== '' && $v !== null);
Relationship filters are whereHas under the hood. They generate correlated subqueries; on large tables an unindexed foreign key turns a 30ms endpoint into a multi-second one. Run EXPLAIN on the generated SQL, not just the Eloquent query.
Watch for N+1 on the eager-load side. Filtering by posts.title returns users whose posts match — but if you render $user->posts, you'll lazy-load all of them unless you constrain the eager load to the same condition.
Debugging: use ->toSql() with ->getBindings() or DB::listen() rather than guessing. When a filter "doesn't work," nine times out of ten the key is misspelled or filtered out by the whitelist — log the post-whitelist array before it reaches the package.
Extension points: look for overridable methods on the trait (applyFilter, filterable, per-column handlers). If custom operators are needed (>=, LIKE), the clean pattern is a small subclass or a wrapper service rather than monkey-patching the vendor file — otherwise composer update (however unlikely for an archived package) wipes your work.
Version compatibility checkpoint: before committing to it, verify the package works with your Laravel and PHP version on a branch. Archived packages frequently use Arr:: helpers or Str:: methods that were removed or changed, and the failure mode is a confusing Call to undefined method deep in a request rather than an install error.
How can I help you explore Laravel packages today?