DEV Community

muhammad reda
muhammad reda

Posted on

How to Implement Scalable Filtering in Laravel with Laravel Sieve

Laravel Sieve: A Better Approach to Query Filtering in Laravel Applications
As Laravel developers, we’ve all faced the challenge of implementing dynamic filtering in our applications. What starts as a simple filter often becomes a tangled web of if-statements and query conditions that make controllers bloated and hard to maintain.

Today, I’m excited to share Laravel Sieve, an open-source package I’ve developed to solve this common problem. It’s a modular, scalable filtering engine designed with clean code principles at its core.

The Problem Laravel Sieve Solves
If you’ve worked on Laravel applications with complex filtering needs (like APIs, dashboards, or reporting systems), you’ve likely faced this issue:

  • Controllers cluttered with endless if statements.
  • Repeated filtering logic across different endpoints.
  • Difficulty maintaining or extending filtering rules.
  • Filter conditions are hard to reuse across different parts of the application.

This violates SOLID and DRY principles and makes the code harder to debug and scale.

Enter Laravel Sieve
Laravel Sieve takes a different approach by isolating filter logic into dedicated classes, following SOLID principles, and making your code more maintainable.

Key Features

  • Decouple filtering logic from controllers.
  • Eliminate repetitive conditions with reusable filter classes.
  • Handle dynamic filtering & multiple sorting effortlessly
  • Keep your codebase clean and maintainable.

Getting Started with Laravel Sieve

Installation

composer require architools/laravel-sieve

Creating a Custom Utilities Service

To begin, define your service class that extends the base UtilitiesService class:

namespace App\Utilities;

use ArchiTools\LaravelSieve\UtilitiesService;

class ProductUtilitiesService extends UtilitiesService
{
}
Enter fullscreen mode Exit fullscreen mode

Defining Filters

Define available filters inside the filters() Method of your service class. Each filter is a key-value pair where:

  • The key represents the query parameter name.
  • The value is either a method name to handle the filter implementation or a Filter instance.
namespace App\Utilities;

use ArchiTools\LaravelSieve\Filters\Joins\Concretes\Join;
use ArchiTools\LaravelSieve\Filters\Conditions\Concretes\{Condition, AggregationCondition};
use ArchiTools\LaravelSieve\UtilitiesService;
use ArchiTools\LaravelSieve\Criteria;

class ProductUtilitiesService extends UtilitiesService
{
    public function filters(): array
    {
        return [
            'category_name' => 'categoryNameFilter',
            'q' => new ProductSearchFilter()
        ];
    }

    public function categoryNameFilter(Criteria $criteria, mixed $value)
    {
        if (!$criteria->joinExists('product_categories')) {
            $criteria->appendJoin(new Join('product_categories', 'products.id', '=', 'product_categories.product_id', 'left'));
        }
        $criteria->appendCondition(new Condition('product_categories.name', 'like', "%$value%"));
    }
}
Enter fullscreen mode Exit fullscreen mode

Each filter method receives two arguments:

Criteria $criteria The criteria instance for appending conditions & joins.
mixed $value The filter value from the request.

Reusable Filter Class

namespace App\Utilities\Filters;

use ArchiTools\LaravelSieve\Filters\Contracts\Filter;
use ArchiTools\LaravelSieve\Criteria;
use ArchiTools\LaravelSieve\Filters\Joins\Concretes\Join;
use ArchiTools\LaravelSieve\Filters\Conditions\Concretes\GroupConditions;
use ArchiTools\LaravelSieve\Filters\Conditions\Concretes\Condition;
use ArchiTools\LaravelSieve\Filters\Conditions\Concretes\JsonContainCondition;

class ProductSearchFilter implements Filter
{
    public function apply(Criteria $criteria, mixed $value)
    {
        if (!$criteria->joinExists('product_categories')) {
            $criteria->appendJoin(new Join('product_categories', 'products.id', '=', 'product_categories.product_id', 'left'));
        }
        $criteria->appendCondition(new GroupConditions([
            new Condition('products.name', 'like', "%{$value}%"),
            new Condition('product_categories.name', 'like', "%{$value}%", 'or'),
            new JsonContainCondition('products.keywords', $value, 'or')
        ]));    
    }
}
Enter fullscreen mode Exit fullscreen mode

Defining Sorts

Define available sorts inside the sorts() Method of your service class. Each sort is a key-value pair where:

  • The key represents the sort name.
  • The value is either the name of a method that returns a BaseSort instance, or A string representing a column name or alias, which will be used directly for sorting.

namespace App\Utilities;

...
use ArchiTools\LaravelSieve\UtilitiesService;
use ArchiTools\LaravelSieve\Sorts\Concretes\RawSort;
use ArchiTools\LaravelSieve\Sorts\Contracts\BaseSort;

class ProductUtilitiesService extends UtilitiesService
{
    ... 

    public function sorts(): array
    {
        return [
            'name' => 'customNameSort',
            'created_at' => 'products.created_at',
        ];
    }

    public function customNameSort(string $direction): BaseSort
    {
        return new RawSort('LENGTH(products.name)', $direction);
    }
}
Enter fullscreen mode Exit fullscreen mode

Request Format

// Single sort
/products?sorts[0][field]=name&sorts[0][direction]=desc

// Multiple sorts
/products?sorts[0][field]=created_at&sorts[0][direction]=desc&sorts[1][field]=name
Enter fullscreen mode Exit fullscreen mode

the sorts key is configurable.

Using the Utilities Service

Inject the service into your controller and apply filters and sorts:


use App\Utilities\ProductUtilitiesService;

class ProductController extends Controller
{
    public function index(Request $request, ProductUtilitiesService $utilitiesService)
    {
        $criteria = $utilitiesService
            ->applyFilters()
            ->applySorts()
            ->getCriteria();

        // Use the criteria in your repository or query builder
    }
}
Enter fullscreen mode Exit fullscreen mode

Building the Query

Utilize the Criteria object to modify the builder instance


use ArchiTools\LaravelSieve\Criteria;
use App\Models\Product;

class ProductRepository
{
    public function getProducts(Criteria $criteria)
    {
        $query = Product::query();
        $criteria->applyOnBuilder($query);

        return $query->get();
    }
}
Enter fullscreen mode Exit fullscreen mode

Benefits for Your Project

By adopting Laravel Sieve, you’ll experience:

  • Cleaner Controllers: Focus on HTTP concerns, not filtering logic.
  • Improved Testability: Each filter can be unit tested in isolation.
  • Better Maintainability: Changes to filtering logic don’t require controller modifications.
  • Enhanced Reusability: Filters can be shared across different models and controllers.
  • Scalable Architecture: As your application grows, your filtering system grows cleanly with it.

Ready to Dive Deeper?

Laravel Sieve is designed to save your time and keep your codebase clean, but this article only scratches the surface!

📖 For full documentation, advanced examples, and contribution guidelines, check out the:
🔗 Laravel Sieve GitHub Repository

📦 Packagist: https://packagist.org/packages/architools/laravel-sieve

Got questions or ideas? Star the repo ⭐, open an issue, or submit a PR — I’d love your feedback!

Top comments (4)

Collapse
 
xwero profile image
david duymelinck • Edited

I recently saw a similar package by @thavarshan

I like your package method of working better because it is detached from the model. @thavarshan package has more features on first glance. So I think it is good there is choice.

I feel that both packages are created because Eloquent doesn't offer reusable optional (local) scopes. Would you have made the package if Eloquent had that option?

PS: I find the name creative.

Collapse
 
muhammad_reda97 profile image
muhammad reda

Hello @xwero

Thanks a lot for the kind words — really glad you found the design choice of detaching from the model useful. I also checked out @thavarshan’s package, and I agree — it’s rich with features like caching, query performance logging, and user input validation.

With Laravel Sieve, my focus is intentionally narrow: just the logic of filtering. I wanted it to be lightweight, highly composable, and framework-consistent — so it integrates naturally with Eloquent or even raw query builders without imposing extra layers. Features like caching, validation, or performance tracking are definitely valuable — but I see them as outside the core scope of this package. Keeping Sieve focused makes it easier to maintain, extend, and adapt to different projects without added complexity or overhead.

That said, I think it’s great that both packages exist with different strengths — it gives developers real choice depending on what matters most for their use case.

And yes — if Eloquent offered reusable, optional local scopes, I might not have needed to build this at all 😄

Appreciate your thoughtful comment — and thanks for the shoutout on the name too!

Collapse
 
bicibg profile image
Bugra Ergin

How does it handle AND / OR logic? What happens if I want to combine multiple filters? Not gonna lie, my experience in every single project is exactly as you describe here. I will definitely give this a go

Collapse
 
muhammad_reda97 profile image
muhammad reda

Hello @bicibg

Great question — Laravel Sieve is built with composability in mind, so combining multiple filters is fully supported. By default, it applies filters using AND logic.
For OR logic, the Condition class accepts a string ('and' or 'or') to define the logical operator, giving you full control over how filters are applied.
You can also use a GroupConditions class that explicitly defines how conditions should be grouped and applied, including support for mixed AND/OR logic.

I totally relate to your experience — that's exactly what inspired me to build Sieve in the first place. Looking forward to your feedback if you give it a try! Let me know if you run into any use cases you’d like better support for.