A search or filter form where several fields are all optional needs conditional query building — when() keeps this readable as a single fluent chain, avoiding the nested if statements and repeatedly reassigned query variable that the naive approach would otherwise require.
The naive approach, without when()
$query = Product::query();
if ($request->filled('category')) {
$query = $query->where('category_id', $request->category);
}
if ($request->filled('min_price')) {
$query = $query->where('price', '>=', $request->min_price);
}
if ($request->filled('sort')) {
$query = $query->orderBy($request->sort);
} else {
$query = $query->orderBy('created_at', 'desc');
}
$products = $query->get();
This works, but it's verbose, and the query-building logic is scattered across multiple if blocks rather than reading as one coherent chain.
The same logic with when()
$products = Product::query()
->when($request->filled('category'), function ($query) use ($request) {
$query->where('category_id', $request->category);
})
->when($request->filled('min_price'), function ($query) use ($request) {
$query->where('price', '>=', $request->min_price);
})
->when(
$request->filled('sort'),
fn ($query) => $query->orderBy($request->sort),
fn ($query) => $query->orderBy('created_at', 'desc')
)
->get();
when($condition, $callback) only runs the callback (applying that specific condition to the query) if the first argument is truthy — otherwise it's simply skipped, leaving the query chain unaffected and moving on to the next when() in the chain.
The optional third argument: an "else" callback
->when($request->filled('sort'),
fn ($query) => $query->orderBy($request->sort),
fn ($query) => $query->orderBy('created_at', 'desc') // runs when the condition is false
)
The sort example above shows this three-argument form — a default sort order applies specifically when no explicit sort was requested, all still within the same single fluent chain rather than needing a separate if/else block.
Passing the condition's value directly into the callback
->when($request->category, function ($query, $category) {
$query->where('category_id', $category);
})
When the first argument to when() is itself the value being checked (rather than a separate boolean expression), that same value is passed as the second argument to the callback — this avoids needing a separate use ($request) closure capture just to re-read the same value again inside the callback.
Using when() with a Form Request's validated data
public function index(ProductFilterRequest $request)
{
$validated = $request->validated();
$products = Product::query()
->when($validated['category'] ?? null, fn ($query, $category) => $query->where('category_id', $category))
->when($validated['min_price'] ?? null, fn ($query, $price) => $query->where('price', '>=', $price))
->paginate(20);
}
Why this pattern matters beyond just style
Beyond readability, when() genuinely reduces the risk of a bug from a mis-assigned or forgotten $query = $query->... reassignment in the naive version — since when() is itself part of the fluent chain, there's no separate variable reassignment step to get wrong.