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 Sieve Laravel Package

architools/laravel-sieve

A Laravel package that adds a lightweight, composable filtering layer to Eloquent queries. Define sieves to sift model results by request parameters, keeping controllers clean and query logic reusable across your app.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and confirm what actually ships. Since this package has very low adoption (14 stars, zero dependents), treat the source as the documentation of record.

composer require architools/laravel-sieve

Then immediately do three things before writing any code:

  1. Open vendor/architools/laravel-sieve/composer.json and check extra.laravel.providers — if a provider is listed, it auto-registers. If not, add it to config/app.php manually.
  2. Look for config/sieve.php in the package and publish it if present (php artisan vendor:publish --provider="Architools\Sieve\SieveServiceProvider" — confirm the exact provider class from the source).
  3. Run php artisan list and grep for sieve — many filter packages ship a make: generator command for filter classes. If one exists, use it; if not, you'll be writing plain classes.

Where to look first. Read src/ top to bottom. It's a small package, and the two things that matter are (a) the trait or facade that hooks into the query builder, and (b) the base filter class/contract your filters must extend or implement. Everything else is glue.

First use case. Take a GET /api/users index endpoint that currently hardcodes a few when() clauses. Replace it with a Sieve-driven query:

// Controller
public function index(Request $request)
{
    $users = User::query()
        ->sieve($request->query())     // confirm method name from the trait
        ->paginate(25);

    return UserResource::collection($users);
}

On the model, add the package trait (typically use Sieveable; or use HasFilters; — check src/Concerns/ or src/Traits/). Declare which filters are permitted, either as a property on the model or in a dedicated filter class. The moment that endpoint responds to ?name=ali&sort=-created_at without you touching the controller again, you've validated the integration.

Start with one endpoint. This package rewards you for centralizing filter definitions, so don't scatter it across ten controllers before you've proven the pattern on one.

Implementation Patterns

Dedicated filter classes per resource. The idiomatic shape is one filter class per model or per endpoint, with each filter as its own method. This is the pattern that pays off daily — adding ?status=active becomes a one-method change in one file rather than a new when() in a controller.

class UserFilter extends Filter   // confirm base class name
{
    public function name(string $value): void
    {
        $this->query->where('name', 'like', "%{$value}%");
    }

    public function status(string $value): void
    {
        $this->query->where('status', $value);
    }

    public function createdAfter(string $value): void
    {
        $this->query->where('created_at', '>=', Carbon::parse($value));
    }
}

Whitelist, always. Whatever the package calls it — $allowed, $filters, $sortable — declare it explicitly. Never let a request parameter reach a where() or orderBy() column name unchecked. A query-string-driven orderBy($request->sort) is a classic enumeration and index-bypass vector. If the package has a default-allow mode, turn it off.

Sorting conventions. Most packages of this shape accept ?sort=name and ?sort=-name (leading hyphen for descending), plus comma lists like ?sort=-created_at,name. Confirm the separator and direction token from the parser in src/ before you publish API docs, because clients will depend on it forever. Pair multi-column sorts with a stable tiebreaker (id) so pagination doesn't duplicate or drop rows:

// if the package supports default/appended sorts, set it in config or the query
->orderBy('id')  // deterministic tiebreaker

Compose, don't override. Filters should only add constraints; let pagination, eager loading, and scoping live in the controller. This keeps the filter class reusable across GET /users, an admin export, and a report job:

$query = User::with('team')->where('tenant_id', $tenantId);
$users = (new UserFilter($query))->apply($request->query())->paginate(25);

Reuse via a base filter + shared filters. For multi-tenant or multi-role apps, put cross-cutting filters (tenant_id, createdAfter, search) in an abstract base filter and extend it. This is the single biggest day-to-day win over inline when() chains.

Integration with API resources. Keep filters query-only. Do not let a filter mutate the model, trigger side effects, or eager-load relationships — relationship filters should use whereHas and leave loading to the controller. Otherwise your export job starts silently loading six relations it doesn't serialize.

Test at the HTTP layer. Write one feature test per filter method, plus a test that an unlisted param is ignored and a test that a malicious ?sort=password is rejected. Because filter behavior is request-driven, HTTP tests catch far more than unit tests on the filter class.

Gotchas and Tips

This is a 14-star, zero-dependent package. There is no ecosystem of Stack Overflow answers, no third-party blog posts, and no guarantee of semver discipline. Pin the version exactly ("architools/laravel-sieve": "1.x" or a commit hash) and read the diff before every update. If the filtering surface is small, price out the cost of forking it into app/Support/ — for a package this size, vendoring is often cheaper than a supply-chain dependency you can't audit.

Method-name-to-param mapping is the sharp edge. Most packages in this family map the query param directly to a filter method name. That means ?created_after= may not reach a method named createdAfter() — check whether the package snake-cases, studly-cases, or requires an explicit alias map. Getting this wrong produces silent no-ops: the request succeeds, returns unfiltered data, and nobody notices until a customer complains.

Empty strings and ?param= with no value. Decide explicitly whether ?status= means "ignore" or "status is empty." Packages differ, and the default is often to filter on '', which returns nothing. Guard in your filter methods with an early return on blank($value) unless you genuinely want to match empty.

Relationship filters are quietly expensive. A whereHas('orders', ...) filter adds a correlated subquery per row. On a table with a couple hundred thousand rows this goes from 40ms to 4 seconds with no obvious cause, because the filter class looks innocent. Check EXPLAIN output the first time you add a relationship filter, and consider whereIn on a pre-fetched ID set for hot paths.

Sorting on unindexed columns. The package will happily orderBy anything you allow. Every column in your sortable whitelist needs an index, or at minimum should be tested against production-scale data. Sorting plus OFFSET pagination on an unindexed column is the most common way these endpoints fall over.

Debugging: dump the SQL, not the filter. When a filter "doesn't work," the fastest path is ->toSql() with bindings before and after applying Sieve:

$q = User::query();
(new UserFilter($q))->apply($request->query());
dump($q->toSql(), $q->getBindings());

If the SQL is identical, the param never reached a filter — it's a naming/whitelist problem, not a filter-logic problem. That distinction saves a lot of time.

Extension points to look for. In a package this size the useful seams are: a custom query-param key (if your API uses filter[name] instead of name=), a resolver hook for mapping filter classes to models, and a config toggle for strict vs. permissive mode. If none of those exist and you need them, a thin wrapper in app/Support/QueryFilters.php that normalizes input before handing off to Sieve is usually a better investment than forking.

Config caching. If the package reads config at boot, php artisan config:cache in production can mask a missing publish step that worked fine locally. Publish the config file as part of deployment, not as an afterthought.

Don't let the request object leak into filter classes. Accept an array of params (or a DTO), not Request. That single discipline keeps filters usable from console commands, queued exports, and tests — which is exactly where you'll want them within a month of adopting this.

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.
socialdept/tenant-domains
tautvydasr/blade-class-names
socialdept/color-scales
maherelgamil/controller
coleus/widgets
coleus/table
coleus/users
coleus/music
coleus/health
coleus/calendar
coleus/notes
coleus/support
coleus/settings
coleus/apps
seampact/laravel
emmyconceptng/laravel-ng-postcode
jjsquady/round-robin
skyraptor/chatter
edlugz/sasapay
abbasudo/artisan-route