php
45 lines · 9 steps
Building a filtered product index in Laravel
How Eloquent's when() method conditionally chains query filters from validated request input.
Explained by
highlit
1<?php
2
3namespace App\Http\Controllers;
4
5use App\Models\Product;
6use Illuminate\Http\Request;
7
8class ProductController extends Controller
9{
10 public function index(Request $request)
11 {
12 $filters = $request->validate([
13 'search' => ['nullable', 'string', 'max:255'],
14 'category_id' => ['nullable', 'integer', 'exists:categories,id'],
15 'min_price' => ['nullable', 'numeric', 'min:0'],
16 'max_price' => ['nullable', 'numeric', 'gte:min_price'],
17 'in_stock' => ['nullable', 'boolean'],
18 'sort' => ['nullable', 'in:newest,price_asc,price_desc'],
19 ]);
20
21 $products = Product::query()
22 ->with('category')
23 ->when($filters['search'] ?? null, function ($query, $search) {
24 $query->where(function ($q) use ($search) {
25 $q->where('name', 'like', "%{$search}%")
26 ->orWhere('sku', 'like', "%{$search}%");
27 });
28 })
29 ->when($filters['category_id'] ?? null, fn ($query, $id) => $query->where('category_id', $id))
30 ->when($filters['min_price'] ?? null, fn ($query, $min) => $query->where('price', '>=', $min))
31 ->when($filters['max_price'] ?? null, fn ($query, $max) => $query->where('price', '<=', $max))
32 ->when($request->boolean('in_stock'), fn ($query) => $query->where('stock', '>', 0))
33 ->when($filters['sort'] ?? 'newest', function ($query, $sort) {
34 match ($sort) {
35 'price_asc' => $query->orderBy('price'),
36 'price_desc' => $query->orderByDesc('price'),
37 default => $query->latest(),
38 };
39 })
40 ->paginate(24)
41 ->withQueryString();
42
43 return view('products.index', compact('products', 'filters'));
44 }
45}
01 / 01
STEP 01
‹ swipe to step through ›
Walkthrough
Space play
←→ step
click any line
Three takeaways
- 1Validating request input up front turns untrusted query parameters into a safe, typed array you can trust downstream.
- 2when() lets you assemble a query conditionally without a tangle of if statements, applying each filter only when its value is present.
- 3Pairing paginate() with withQueryString() keeps active filters intact as users move between pages.
Related explainers
typescript
import { NestFactory } from '@nestjs/core'; import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'; import { ValidationPipe } from '@nestjs/common'; import { ApiProperty } from '@nestjs/swagger';
Wiring validation and Swagger docs in NestJS
validation
openapi
decorators
Intermediate
8 steps
javascript
const ROLE_PERMISSIONS = { admin: ['users:read', 'users:write', 'billing:read', 'billing:write'], manager: ['users:read', 'billing:read'], member: ['users:read'],
Role-based permissions middleware in Express
authorization
middleware
rbac
Intermediate
9 steps
php
<?php namespace App\Console\Commands;
Releasing stale document locks in Laravel
artisan-command
transactions
row-locking
Intermediate
6 steps
php
<?php namespace App\Providers;
Subdomain multi-tenancy routing in Laravel
multi-tenancy
service-container
route-binding
Advanced
7 steps
ruby
module Paginatable extend ActiveSupport::Concern private
A reusable pagination concern in Rails
pagination
concerns
http-headers
Intermediate
8 steps
php
<?php namespace App\Broadcasting;
Authorizing presence channels in Laravel
broadcasting
authorization
presence-channels
Intermediate
3 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/building-a-filtered-product-index-in-laravel-explained-php-ff18/embed?autoplay=1" width="100%" height="520" loading="lazy" style="border:0"></iframe>
Autoplay is on by default — add ?autoplay=0 to start paused.