Laravel Magazine
Conditional Query Building with when() and unless()

Conditional Query Building with when() and unless()

Eric Van Johnson ·

Filtered index pages are where query building gets ugly. You have a handful of optional filters from the request, and the obvious code breaks the fluent chain with an if for each one. Laravel's when() and unless() methods let you apply a constraint only when a value is truthy, keeping the whole query in a single readable expression.

The Verbose Version

Here is the pattern most of us write first, filtering products by an optional search term and status:

$query = Product::query();

if ($request->filled('search')) {
    $query->where('name', 'like', "%{$request->search}%");
}

if ($request->filled('status')) {
    $query->where('status', $request->status);
}

$products = $query->latest()->paginate();

It works, but the chain is broken into pieces and the intent is buried in control flow.

The when() Version

when() takes a value and a closure. If the value is truthy, it runs the closure, passing the query and the value itself. If the value is falsy, it does nothing and returns the query untouched:

$products = Product::query()
    ->when($request->search, function ($query, $search) {
        $query->where('name', 'like', "%{$search}%");
    })
    ->when($request->status, function ($query, $status) {
        $query->where('status', $status);
    })
    ->latest()
    ->paginate();

The value is passed into the closure as the second argument, so you do not have to reach back to the request inside the closure. That second argument is easy to miss and cleans things up nicely.

The Default Callback

when() accepts an optional third argument: a closure that runs when the value is falsy. This is the equivalent of an else branch:

$products = Product::query()
    ->when($request->sort, function ($query, $sort) {
        $query->orderBy($sort);
    }, function ($query) {
        $query->latest();
    })
    ->paginate();

unless() Is the Inverse

unless() is exactly when() with the condition flipped. It runs the closure when the value is falsy. Reach for it when the natural reading of your condition is negative:

// Only show unpublished drafts to guests when they explicitly ask
$posts = Post::query()
    ->unless($request->user(), function ($query) {
        $query->where('published', true);
    })
    ->get();

A Word of Caution

The value you pass is evaluated for truthiness, so watch out for the integer 0, the string '0', and empty strings, which are all falsy. If a legitimate filter value could be zero, guard with something explicit like $request->filled('key') and pass a boolean instead. Used with that caveat in mind, when() and unless() turn a wall of conditionals into a chain that reads like the query it builds.

Further Reading

Stay Updated

Subscribe to our newsletter

Get latest news, tutorials, community articles and podcast episodes delivered to your inbox.

Weekly articles
We send a new issue of the newsletter every week on Friday.
No spam
We'll never share your email address and you can opt out at any time.