How to Build a Data Filter Feature in Laravel

Data filtering and searching is a very common need in web applications — from product filters in e-commerce to report searches in management systems. In Laravel, there are several...

How to Build a Data Filter Feature in Laravel

Data filtering and searching is a very common need in web applications — from product filters in e-commerce to report searches in management systems. In Laravel, there are several ways to implement it, from the simplest to the most structured. This article will teach you a clean, easily extensible data filter pattern that doesn't clutter the controller.

The Simple Approach: Filter Directly in the Controller

The most basic way is to use conditionals in the query builder. Suitable for filters that aren't too numerous:

<?php

namespace App\Http\Controllers;

use App\Models\Product;
use Illuminate\Http\Request;

class ProductController extends Controller
{
    public function index(Request $request)
    {
        $query = Product::query();

        // Filter by name (search)
        if ($request->filled('search')) {
            $query->where('name', 'like', '%' . $request->search . '%');
        }

        // Filter by category
        if ($request->filled('category_id')) {
            $query->where('category_id', $request->category_id);
        }

        // Filter by price range
        if ($request->filled('min_price')) {
            $query->where('price', '>=', $request->min_price);
        }
        if ($request->filled('max_price')) {
            $query->where('price', '<=', $request->max_price);
        }

        // Sorting
        $sortBy  = $request->get('sort_by', 'created_at');
        $sortDir = $request->get('sort_dir', 'desc');
        $allowedSorts = ['name', 'price', 'created_at'];

        if (in_array($sortBy, $allowedSorts)) {
            $query->orderBy($sortBy, $sortDir === 'asc' ? 'asc' : 'desc');
        }

        $products = $query->paginate(15)->withQueryString();

        return view('products.index', compact('products'));
    }
}

Creating the Filter Form in Blade

<form method="GET" action="{{ route('products.index') }}">
    <input type="text" name="search" value="{{ request('search') }}" placeholder="Search products...">

    <select name="category_id">
        <option value="">All Categories</option>
        @foreach($categories as $cat)
            <option value="{{ $cat->id }}" {{ request('category_id') == $cat->id ? 'selected' : '' }}>
                {{ $cat->name }}
            </option>
        @endforeach
    </select>

    <input type="number" name="min_price" value="{{ request('min_price') }}" placeholder="Min price">
    <input type="number" name="max_price" value="{{ request('max_price') }}" placeholder="Max price">

    <button type="submit">Filter</button>
    <a href="{{ route('products.index') }}">Reset</a>
</form>

Use withQueryString() on the paginator so the filter parameters persist when moving between pages.

A Cleaner Approach: Local Scopes in the Model

For reusable filters, use Eloquent local scopes:

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;

class Product extends Model
{
    public function scopeSearch(Builder $query, ?string $search): Builder
    {
        return $query->when($search, function ($q) use ($search) {
            $q->where('name', 'like', "%{$search}%")
              ->orWhere('description', 'like', "%{$search}%");
        });
    }

    public function scopeCategory(Builder $query, ?int $categoryId): Builder
    {
        return $query->when($categoryId, fn($q) => $q->where('category_id', $categoryId));
    }

    public function scopePriceRange(Builder $query, ?float $min, ?float $max): Builder
    {
        return $query
            ->when($min, fn($q) => $q->where('price', '>=', $min))
            ->when($max, fn($q) => $q->where('price', '<=', $max));
    }
}

Usage in the controller becomes very clean:

$products = Product::query()
    ->search($request->search)
    ->category($request->category_id)
    ->priceRange($request->min_price, $request->max_price)
    ->latest()
    ->paginate(15)
    ->withQueryString();

The Professional Approach: A Separate Filter Class

For large-scale applications with many filters, create a dedicated filter class so the controller stays thin:

<?php
// app/Filters/ProductFilter.php

namespace App\Filters;

use Illuminate\Database\Eloquent\Builder;
use Illuminate\Http\Request;

class ProductFilter
{
    public function __construct(protected Request $request) {}

    public function apply(Builder $query): Builder
    {
        if ($this->request->filled('search')) {
            $search = $this->request->search;
            $query->where(function ($q) use ($search) {
                $q->where('name', 'like', "%{$search}%")
                  ->orWhere('sku', $search);
            });
        }

        if ($this->request->filled('category_id')) {
            $query->where('category_id', $this->request->category_id);
        }

        if ($this->request->filled('min_price')) {
            $query->where('price', '>=', $this->request->min_price);
        }

        if ($this->request->filled('max_price')) {
            $query->where('price', '<=', $this->request->max_price);
        }

        if ($this->request->filled('in_stock')) {
            $query->where('stock', '>', 0);
        }

        return $query;
    }
}

Use it in the controller with injection:

use App\Filters\ProductFilter;

public function index(Request $request, ProductFilter $filter)
{
    $products = $filter->apply(Product::query())
        ->latest()
        ->paginate(15)
        ->withQueryString();

    return view('products.index', compact('products'));
}

Preserving Filter State in the View

Make sure the filter values stay filled in after the form is submitted by using the request() helper:

<input type="text"
       name="search"
       value="{{ request('search') }}"
       placeholder="Search by name or SKU...">

Conclusion

Choose the approach that fits your project's complexity. For simple filters, doing it directly in the controller is enough. For more complex and reusable ones, use local scopes in the model. If there are already very many filters that need to be used in many places, separate them into a dedicated Filter class. With the right pattern, your filter feature will be easy to extend and maintain without cluttering code everywhere.

filter data laravel laravel query filter eloquent scope laravel laravel search filter filter tabel laravel query builder laravel
Share this article
Back to Blog
🚀 Partner Recommendation

Need Premium Source Code & Business Apps?

Access Laravel applications, POS systems, School Management, Clinic Software, ERP solutions, and ready-to-use premium source code at GudangCode.

GudangCode
  • ✔ Premium Source Code
  • ✔ Ready-to-Use Systems
  • ✔ Lifetime Updates
  • ✔ Lifetime Membership
  • ✔ Daily App Updates
Join Membership →