Collection::sole
Returns the one item matching a test, throwing if zero or more than one match.
Collection::sole(callable|string|null $key = null, mixed $operator = null, mixed $value = null): mixed- Added in Laravel 8.x
Parameters
| Parameter | Type | Description |
|---|---|---|
$$keyoptional | callable|string|null= null | A callback invoked as ($value, $key), or the field name for a key/value comparison. Omit it to require that the collection holds exactly one item. |
$$operatoroptional | mixed= null | With two arguments this is the value to compare against; with three it is the comparison operator, such as '>' or '!='. |
$$valueoptional | mixed= null | The value to compare the field against when an operator is supplied. |
Return value
mixedThe single item that passes the test. With no arguments, the only item in the collection.
Errors
Throws Illuminate\Support\ItemNotFoundException when nothing matches, and Illuminate\Support\MultipleItemsFoundException (message "N items were found.") when more than one does.
This is first() with the ambiguity removed. first() quietly hands back whichever match came earliest, so a duplicate that should never have existed becomes a silent wrong answer; sole() refuses and throws. Reach for it wherever your data model says exactly one: the primary email address, the active subscription, the default shipping address. It turns a data-integrity bug into a loud failure at the point it occurs.
Two different exceptions come out of it, and both live in Illuminate\Support despite the class files sitting under Collections. ItemNotFoundException carries no message at all, so logging $e->getMessage() gives you an empty string — log the class name or add your own context. MultipleItemsFoundException reports the count but not the items.
Calling forms
With no arguments it asserts the whole collection holds one item. One argument is a callback or a truthy field name. Two arguments compare a field for equality; three insert an operator between them. Note that comparisons use data_get(), so dot notation into nested arrays works.
In an HTTP context the framework converts ItemNotFoundException into a 404, which is usually what you want.
Example
When to use it
- Fetch the single primary email address for a user and fail loudly if two exist.
- Pull the one active subscription from a loaded relation before charging it.
- Validate an import file that must contain exactly one row per reference code.
Frequently asked questions
What is the difference between sole and first?
first() returns the earliest match and ignores any others, while sole() throws MultipleItemsFoundException if more than one item matches and ItemNotFoundException if none does. Use sole() when more than one match means your data is wrong.
Which exceptions does sole throw and where do they live?
It throws Illuminate\Support\ItemNotFoundException for no match and Illuminate\Support\MultipleItemsFoundException for more than one. Both are in the Support namespace even though the class files sit in the Collections directory.
Why is the exception message empty?
ItemNotFoundException is constructed without a message, so getMessage() returns an empty string. Log the exception class and your own context; MultipleItemsFoundException does carry a message and a getCount().
More examples
All four calling forms
Prints B2, 3, A1, A1 and only. The comparison forms run through data_get(), which is why 'meta.tier' reaches into the nested array. The no-argument form is a compact way to assert that a filtered collection collapsed to a single row.
<?php
$rows = collect([
['sku' => 'A1', 'qty' => 3, 'meta' => ['tier' => 'gold']],
['sku' => 'B2', 'qty' => 9, 'meta' => ['tier' => 'silver']],
]);
// 1. Callback.
echo $rows->sole(fn (array $r) => $r['qty'] > 5)['sku'], PHP_EOL;
// 2. Field and value.
echo $rows->sole('sku', 'A1')['qty'], PHP_EOL;
// 3. Field, operator, value.
echo $rows->sole('qty', '<', 5)['sku'], PHP_EOL;
// 4. Dot notation into nested data.
echo $rows->sole('meta.tier', 'gold')['sku'], PHP_EOL;
// 5. No arguments: the collection must hold exactly one item.
echo collect(['only'])->sole(), PHP_EOL;Guarding a data-integrity assumption
Shows why the two exceptions are worth catching separately: two primaries is a corrupt-data alarm, none is an onboarding gap. MultipleItemsFoundException exposes getCount(); ItemNotFoundException carries nothing, so the user id has to be added by hand.
<?php
namespace App\Services;
use App\Models\User;
use Illuminate\Support\ItemNotFoundException;
use Illuminate\Support\MultipleItemsFoundException;
use Illuminate\Support\Facades\Log;
class Mailer
{
public function primaryAddress(User $user): string
{
try {
return $user->emailAddresses->sole('is_primary', true)->address;
} catch (MultipleItemsFoundException $e) {
Log::critical('User has multiple primary addresses', [
'user_id' => $user->id,
'count' => $e->getCount(),
]);
throw $e;
} catch (ItemNotFoundException $e) {
// getMessage() is empty here, so supply the context yourself.
Log::warning('User has no primary address', ['user_id' => $user->id]);
throw $e;
}
}
}sole versus first versus firstOrFail
first() returns the pro plan with no complaint, which is exactly how duplicate-row bugs reach production. sole() prints '2 items were found.' and stops, surfacing the problem where it is cheap to fix.
<?php
$subscriptions = collect([
['plan' => 'pro', 'active' => true],
['plan' => 'team', 'active' => true],
]);
// Silently picks the first of two active plans — a latent billing bug.
print_r($subscriptions->first(fn (array $s) => $s['active']));
// Refuses, because 'the active subscription' is not well defined here.
try {
$subscriptions->sole(fn (array $s) => $s['active']);
} catch (\Illuminate\Support\MultipleItemsFoundException $e) {
echo $e->getMessage(), PHP_EOL;
}
Discussion