php 70 lines · 7 steps

Composable query filters with Laravel's Pipeline

Break a filtered index query into single-purpose classes and pass an Eloquent builder through them one stage at a time.

Explained by highlit
1namespace App\Http\Filters;
2 
3use Closure;
4use Illuminate\Database\Eloquent\Builder;
5 
6class FilterByStatus
7{
8 public function handle(Builder $query, Closure $next): Builder
9 {
10 if ($status = request('status')) {
11 $query->whereIn('status', (array) $status);
12 }
13 
14 return $next($query);
15 }
16}
17 
18class FilterBySearch
19{
20 public function handle(Builder $query, Closure $next): Builder
21 {
22 if ($term = request('q')) {
23 $query->where(function (Builder $q) use ($term) {
24 $q->where('title', 'like', "%{$term}%")
25 ->orWhere('description', 'like', "%{$term}%");
26 });
27 }
28 
29 return $next($query);
30 }
31}
32 
33class FilterByPriceRange
34{
35 public function handle(Builder $query, Closure $next): Builder
36 {
37 $query
38 ->when(request('min_price'), fn (Builder $q, $min) => $q->where('price', '>=', $min))
39 ->when(request('max_price'), fn (Builder $q, $max) => $q->where('price', '<=', $max));
40 
41 return $next($query);
42 }
43}
44 
45namespace App\Http\Controllers;
46 
47use App\Http\Filters\FilterByPriceRange;
48use App\Http\Filters\FilterBySearch;
49use App\Http\Filters\FilterByStatus;
50use App\Models\Product;
51use Illuminate\Support\Facades\Pipeline;
52 
53class ProductController extends Controller
54{
55 public function index()
56 {
57 $products = Pipeline::send(Product::query())
58 ->through([
59 FilterByStatus::class,
60 FilterBySearch::class,
61 FilterByPriceRange::class,
62 ])
63 ->thenReturn()
64 ->latest()
65 ->paginate(15)
66 ->withQueryString();
67 
68 return view('products.index', compact('products'));
69 }
70}
01 / 01
STEP 01

Walkthrough

Space play step click any line
Three takeaways
  1. 1The pipeline pattern turns tangled conditional query logic into a list of small, independently testable stages.
  2. 2Each filter mutates the shared Builder only when its request parameter is present, so absent filters are harmless no-ops.
  3. 3Because every stage receives and returns the same Builder, you can reorder or add filters without touching the controller's core flow.

Related explainers

Share this explainer

Here's the card — post it anywhere.

Composable query filters with Laravel's Pipeline — share card
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code