Reader Stacks

Building Conditional Eloquent Queries With Laravel's when() Method

when() keeps a filterable query readable as one fluent chain, avoiding the nested if statements and reassigned query variable that conditional filtering would otherwise require.

Building Conditional Eloquent Queries With Laravel's when() Method

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.