Collection::firstWhere
Returns the first collection item matching a key/value or key/operator/value comparison.
Collection::firstWhere(callable|string $key, mixed $operator = null, mixed $value = null): TValue|nullParameters
| Parameter | Type | Description |
|---|---|---|
$key | callable|string | The item key to read, using "dot" notation for nested data. A callable is used directly as the truth test. |
$operatoroptional | mixed= null | With three arguments this is the comparison operator (=, ==, !=, <>, <, >, <=, >=, ===, !==). With two arguments it is treated as the value and the operator defaults to =. |
$valueoptional | mixed= null | The value to compare against when an explicit operator is given. |
Return value
TValue|nullThe first item whose resolved key satisfies the comparison, or null when nothing matches. There is no default parameter.
firstWhere is shorthand for first() with a generated comparison closure. It reads one key out of each item and returns the first item where the comparison holds, so the common "find the row whose status is pending" lookup needs no callback at all.
Argument counting decides the meaning
The method inspects how many arguments you actually passed. Two arguments mean key and value with an implicit =. Three mean key, operator and value. One argument means "the first item where this key is truthy" — the value defaults to true. That last form is easy to trigger by accident when a variable holding your comparison value is unexpectedly absent.
The thing that trips people up
The default comparison is loose ==, not ===. Searching for the integer 5 will happily match the string '5', and searching for 0 can match unrelated values under PHP's coercion rules. Pass '===' explicitly when the type matters.
The key is resolved with data_get, so 'user.role' reaches into nested arrays and objects. And unlike first, there is no default argument — a miss is always null, so wrap the call or check the result before dereferencing it.
Example
When to use it
- Pull the row with a given id out of a collection you already fetched, instead of issuing another query.
- Find the first record above a threshold using an explicit >= operator.
- Reach into nested payload data with dot notation, such as firstWhere('user.role', 'admin').
Frequently asked questions
Why does firstWhere match a string when I searched for a number?
Because the default operator is loose ==, which coerces types before comparing. Pass '===' as the second argument and the value as the third to force a strict, type-aware comparison.
Can firstWhere return a default when nothing matches?
No — it returns null and has no default parameter. Use the null-coalescing operator on the result, or use first() with a truth test, which does accept a default as its second argument.
What does firstWhere do with only one argument?
It returns the first item whose value at that key is truthy, because the value defaults to true with an = operator. That makes firstWhere('age') a concise way to skip rows where the field is null, 0 or an empty string.
More examples
Loose versus strict comparison
The first var_dump prints string(1) "5" and the second prints int(5). This is the failure people report as a bug: identifiers arriving from a query string or a JSON body are strings, so a loose lookup can return a different row than the one you expected. Pass '===' whenever the collection can hold both types.
<?php
$mixed = collect([
['n' => '5'],
['n' => 5],
]);
// Default operator is loose ==, so the STRING is matched first.
var_dump($mixed->firstWhere('n', 5)['n']);
// '===' forces a type-aware comparison.
var_dump($mixed->firstWhere('n', '===', 5)['n']);Dot notation into nested data
The key is resolved with data_get, so dotted paths traverse nested arrays and objects alike. The first call prints the row with id 2. The second prints NULL rather than raising an undefined-index warning, because data_get returns null for any missing segment along the path.
<?php
$rows = collect([
['id' => 1, 'user' => ['role' => 'editor']],
['id' => 2, 'user' => ['role' => 'admin']],
]);
print_r($rows->firstWhere('user.role', 'admin'));
// A missing nested key simply never matches.
var_dump($rows->firstWhere('user.team.name', 'core'));Guarding against a null result
Both lines print USD. firstWhere has no default parameter, so either null-coalesce the result or drop down to first(), which accepts one. The second form is worth the extra verbosity when the fallback is a whole record rather than a single field.
<?php
$settings = collect([
['key' => 'timezone', 'value' => 'UTC'],
['key' => 'locale', 'value' => 'en'],
]);
$row = $settings->firstWhere('key', 'currency');
echo $row['value'] ?? 'USD', PHP_EOL;
// Or fall back to first(), which does take a default.
echo $settings->first(fn ($r) => $r['key'] === 'currency', ['value' => 'USD'])['value'], PHP_EOL;
Discussion