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
{
}
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 amethod name
to handle the filter implementation or aFilter 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%"));
}
}
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')
]));
}
}
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 thename
of amethod
that returns aBaseSort
instance, or A string representing acolumn name
oralias
, 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);
}
}
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
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
}
}
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();
}
}
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)
I recently saw a similar package by @thavarshan
Introducing Filterable: A Powerful, Modular Query Filtering System for Laravel
Jerome Thayananthajothy ・ May 13
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.
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!
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
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.