ukfast/laravel-sieve
ukfast/laravel-sieve adds declarative, reusable filtering and sorting to Laravel Eloquent queries, letting you map request query parameters to model scopes for cleaner, safer list endpoints and APIs.
ans-group/laravel-sieve gives you convention-driven filtering, sorting, sparse fieldsets, relationship includes, and pagination straight from the query string — the Laravel equivalent of JSON:API-style query params, but for Eloquent.
v2.0.0 is a platform-support major. The release note only mentions "PHP 8.5 and Laravel 13", which almost certainly means the composer.json constraints were raised. Before anything else, check the diff for composer.json:
composer require ans-group/laravel-sieve:^2.0
composer show ans-group/laravel-sieve # confirm the php/laravel/illuminate constraints
If you are still on PHP 8.2/8.3 or Laravel 11/12, stay on ^1.2 — a major bump is the maintainer's signal that older runtimes are no longer tested, even if the note doesn't spell out a drop.
Minimal setup:
php artisan vendor:publish --tag=sieve-config (check the tag in the README — the config holds defaults for per-page size, max per-page, allowed relations, and the query-parameter names).config/sieve.php first. Everything you'll want to tune day-to-day (default page size, the cap on per_page, the parameter names for filter/sort/include/fields/scope) lives there.First use case — replacing a hand-rolled ->when($request->name, ...) index endpoint:
// app/Http/Controllers/Api/UserController.php
public function index()
{
return UserResource::collection(
Sieve::for(User::class)->paginate()
);
}
That single call reads filter, sort, include, fields, and pagination params, validates them against the model's allow-list, and returns a paginator. GET /users?filter[status]=active&sort=-created_at&include=posts&fields[users]=id,name&per_page=25 works immediately.
Model-level allow-lists. Keep the contract next to the model so it's reviewable in the same PR as schema changes:
class User extends Model
{
public array $sieve = [
'filters' => ['name', 'email', 'status', 'created_at'],
'sorts' => ['name', 'created_at'],
'includes'=> ['posts', 'posts.comments'],
'fields' => ['id', 'name', 'email', 'status', 'created_at'],
];
}
Confirm the exact property/attribute name against the README for your version — it has changed shape across releases, and a silently-ignored allow-list is the most common "why is my filter returning everything?" bug.
Base controller / trait for consistency. Nearly every index endpoint looks the same. Extract it so pagination caps and response shape stay uniform:
trait SievesIndex
{
protected function sieved(string $model)
{
return Sieve::for($model)
->when(! auth()->user()?->isAdmin(), fn ($s) => $s->where('owner_id', auth()->id()))
->paginate();
}
}
Scopes as named business filters. Rather than exposing raw columns for complex cases (e.g. "overdue"), expose a scope: ?scope=overdue. That keeps the query-string surface small and lets you change the SQL later without breaking clients.
Sparse fieldsets to shrink payloads. fields[users]=id,name is the cheapest win for mobile clients and slow connections — pair it with API Resources that already respect $request->input('fields') or let Sieve trim the selected columns.
Relationship includes. ?include=posts.comments is the natural fix for N+1s in list endpoints, because clients declare what they need and Sieve eager-loads only allow-listed relations. Always bound include depth in config — unbounded nesting is a DoS vector.
Pair with policies, not config. Authorization belongs in a Policy/gate applied to the sieved query, not in the allow-list. The allow-list says "this column exists"; the policy says "this user may see it."
Form Requests for the non-Sieve bits. Validate the params Sieve doesn't own (e.g. date-range custom filters) in a FormRequest so failures return a clean 422 instead of a 500 from the query builder.
composer.json for dropped PHP/Laravel versions, bump your CI matrix (add 8.5 + Laravel 13 jobs), run composer why-not if resolution stalls, and re-run your existing feature tests — the platform bump itself should be behaviour-neutral, so any test failure is a real regression, not "just the upgrade."per_page is user-controlled. Always set max_per_page in config. A client asking for 10,000 rows will happily take your database down.WHERE LOWER(name) LIKE '%…%' on a 5M-row table. Check EXPLAIN on your top filters and add indexes; consider restricting LIKE filters to a separate scope.->toSql() / ->dd() on the sieved builder, or enable query logging, when a filter "doesn't work" — nine times out of ten it's a missing allow-list entry or a param-name mismatch between client and config/sieve.php.include + pagination. Eager-loading a large relation across a paginated page is fine; eager-loading it with per_page=500 is not. Cap page size before you cap include depth.sort=posts.created_at requires a join; verify the package version supports it and that the join doesn't multiply rows. Prefer sorts on the primary table.filter[a]&filter[b] vs filter[b]&filter[a]) or you'll get cache misses and inconsistent hit rates.fields. A sparse fieldset request for a column not in the allow-list should error or be stripped — never silently pass through to select().^2.0 in composer.json plus a CI job on PHP 8.5 / Laravel 13 is all you need; but for libraries that must support older Laravel, keep ^1.2 || ^2.0 with a documented matrix.How can I help you explore Laravel packages today?