Collection::when

Runs a callback on the collection only when the given value is truthy, keeping the chain.

Signature
public function when(mixed $value = null, ?callable $callback = null, ?callable $default = null): static|mixed

Parameters

ParameterTypeDescription
$valueoptional(\Closure($this): mixed)|mixed|null= nullThe condition. A Closure is invoked with the collection first and its result is used, so the test can be computed lazily.
$callbackoptional(callable($this, mixed): mixed)|null= nullRun when the value is truthy. Receives the collection and the resolved value.
$defaultoptional(callable($this, mixed): mixed)|null= nullRun when the value is falsy. Receives the same two arguments.

Return value

$this|TWhenReturnTypeWhatever the executed callback returns, or the collection itself when the callback returns null or when neither branch runs. Called with no arguments it returns a HigherOrderWhenProxy instead.

when comes from the Conditionable trait, shared with the query builder, Str and HTTP client — so the pattern you learn here transfers. It resolves the condition (invoking it first if it is a Closure), runs $callback when truthy or $default when falsy, and returns the collection unchanged if neither applies.

When to reach for it

Optional filters. Building a result from request input normally means breaking the chain into a variable and an if; when($request->max, fn ($c, $max) => $c->where('price', '<=', $max)) keeps it one expression. The resolved condition arrives as the callback's second argument, so you rarely need to close over the variable.

What trips people up

Two things, both quiet. First, the test is plain truthiness: 0, '0', '' and [] all skip the callback. A filter like when($minScore, ...) silently stops working when the legitimate value is zero — write when(! is_null($minScore), ...) instead.

Second, the callback must return. The implementation is $callback($this, $value) ?? $this, so a closure that forgets return yields null and when hands back the original collection as if the branch never ran. Arrow functions return implicitly and sidestep this; multi-line closures do not.

Example

Try it yourself
Loading editor…
Press Run to execute the code.

When to use it

  • Apply a search filter only when the request actually supplied one
  • Sort a list one way or another based on a user preference
  • Add a debug-only step to a pipeline without splitting the chain
  • Cap a result set when a limit parameter is present

Frequently asked questions

How do I apply a filter conditionally in a Laravel chain?

Wrap it in when(): $collection->when($condition, fn ($c) => $c->where(...)). The callback runs only when the condition is truthy, and the collection passes through untouched otherwise, so the chain never has to be broken.

Why is my when() callback not doing anything?

Either the condition is falsy — remember 0, '0', '' and [] all count as false — or the callback did not return the modified collection. when() falls back to the original collection when the callback returns null, so a multi-line closure missing its return statement fails silently.

What is the opposite of when() on a collection?

unless(), which runs its callback when the condition is falsy. Both come from the Conditionable trait and take the same three arguments, including the optional default branch.

More examples

The falsy-zero trap

The first chain returns all three rows because when() saw 0 and treated it as "no filter". Any value that is legitimately zero, an empty string or an empty array hits this. Testing !is_null() separates "the parameter was not supplied" from "the parameter is zero", and the closure then reads the variable directly.

Example · php
<?php

$rows = collect([
    ['name' => 'Ada',   'score' => 0],
    ['name' => 'Grace', 'score' => 40],
    ['name' => 'Alan',  'score' => 90],
]);

$minimum = 0;   // a real, meaningful filter value

// Broken: 0 is falsy, so the filter is skipped entirely.
echo $rows->when($minimum, fn ($c, $min) => $c->where('score', '>', $min))
          ->count(), "\n";

// Fixed: test for presence, not truthiness.
echo $rows->when(! is_null($minimum), fn ($c) => $c->where('score', '>', $minimum))
          ->count(), "\n";

// Output:
// 3
// 2

The default branch and a Closure condition

The first call shows the else branch. The second shows a lazily computed condition: the closure receives the collection, sums it to 60, and the map runs. The third is unless(), which fires on a falsy condition and saves you writing when(! $x, ...).

Example · php
<?php

$collection = collect([1, 2, 3]);

// Third argument runs when the condition is falsy.
print_r(
    $collection->when(
        false,
        fn ($c) => $c->push(4),
        fn ($c) => $c->push(5),
    )->all()
);

// A Closure condition is invoked with the collection.
print_r(
    collect([10, 20, 30])
        ->when(fn ($c) => $c->sum() > 50, fn ($c) => $c->map(fn ($n) => $n / 10))
        ->all()
);

// unless() is the inverse of when().
print_r(collect(['a'])->unless(false, fn ($c) => $c->push('b'))->all());

// Output:
// Array ( [0] => 1 [1] => 2 [2] => 3 [3] => 5 )
// Array ( [0] => 1 [1] => 2 [2] => 3 )
// Array ( [0] => a [1] => b )

Optional request filters

when() is the same method on the query builder and on the collection, because both use the Conditionable trait — so one idiom covers the whole chain across the SQL/PHP boundary. Note filled() rather than the bare value on the second call: a submitted "0" would be falsy, and filled() asks whether the input is present instead. Needs a booted application with a database.

Example · php
<?php

namespace App\Http\Controllers;

use App\Models\Product;
use Illuminate\Http\Request;

class ProductController extends Controller
{
    public function index(Request $request)
    {
        return Product::query()
            ->when($request->search, fn ($q, $term) => $q->where('name', 'like', "%{$term}%"))
            ->when($request->filled('in_stock'), fn ($q) => $q->where('stock', '>', 0))
            ->get()
            ->when($request->boolean('cheapest'), fn ($c) => $c->sortBy('price'))
            ->values();
    }
}

Discussion

  • Be the first to comment on this lesson.