Collection::when
Runs a callback on the collection only when the given value is truthy, keeping the chain.
public function when(mixed $value = null, ?callable $callback = null, ?callable $default = null): static|mixedParameters
| Parameter | Type | Description |
|---|---|---|
$valueoptional | (\Closure($this): mixed)|mixed|null= null | The 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= null | Run when the value is truthy. Receives the collection and the resolved value. |
$defaultoptional | (callable($this, mixed): mixed)|null= null | Run 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
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.
<?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
// 2The 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, ...).
<?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.
<?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