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
‹ swipe to step through ›
Walkthrough
Space play
←→ step
click any line
Three takeaways
- 1The pipeline pattern turns tangled conditional query logic into a list of small, independently testable stages.
- 2Each filter mutates the shared Builder only when its request parameter is present, so absent filters are harmless no-ops.
- 3Because every stage receives and returns the same Builder, you can reorder or add filters without touching the controller's core flow.
Related explainers
php
<?php class NameParser {
Parsing a full name into components in PHP
string-parsing
arrays
normalization
Intermediate
8 steps
php
<?php namespace App\Services\Checkout;
Validating coupons with Laravel's Pipeline
pipeline
chain of responsibility
transactions
Intermediate
7 steps
php
<?php namespace App\Services;
How a password strength validator works in PHP
validation
regular-expressions
data-driven
Intermediate
8 steps
php
<?php namespace App\Services;
Building a cached daily leaderboard in Laravel
caching
aggregation
eager-loading
Intermediate
9 steps
php
<?php final class RotatingFileLogger {
How a rotating file logger works in PHP
logging
file-rotation
io
Intermediate
9 steps
php
class TwoFactorController extends Controller { public function show(Request $request): View|RedirectResponse {
Two-factor auth challenge flow in Laravel
authentication
two-factor
middleware
Intermediate
10 steps
Share this explainer
Here's the card — post it anywhere.
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code
Embed this explainer
Drop the interactive walkthrough into a blog or docs. Views never cost a credit.
<iframe src="https://highlit.co/explainers/composable-query-filters-with-laravel-s-pipeline-explained-php-8f98/embed?autoplay=1" width="100%" height="520" loading="lazy" style="border:0"></iframe>
Autoplay is on by default — add ?autoplay=0 to start paused.