Collection::unique
Removes duplicate items, optionally comparing one key or a computed value per row.
public function unique(callable|string|null $key = null, bool $strict = false): staticParameters
| Parameter | Type | Description |
|---|---|---|
$keyoptional | (callable(TValue, TKey): mixed)|string|null= null | A key to compare on each row (dot notation supported), or a closure returning the value that decides uniqueness. Omit to compare the items themselves. |
$strictoptional | bool= false | Compare with === instead of ==. Passing true is equivalent to calling uniqueStrict(). |
Return value
staticA new collection keeping the first occurrence of each distinct value, with the original keys preserved.
unique keeps the first occurrence of each distinct value and discards later ones. With no arguments it defers to array_unique with SORT_REGULAR; with a key or closure it walks the collection tracking values it has already seen and rejects repeats.
When to reach for it
Deduplicating a merged list of IDs, collapsing rows to one per customer, or building a distinct set of tags. The key form is the workhorse — unique('brand') reads far better than a manual seen array — and the closure form handles composite keys: unique(fn ($r) => $r['brand'].'|'.$r['type']).
What trips people up
Comparison is loose by default, so the integer 1 and the string '1' are the same item, and so are 0 and false. On mixed-type data — IDs from a form, values decoded from JSON — that quietly removes rows you meant to keep. Use uniqueStrict() (or pass true) for ===.
Keys are preserved, which surprises people twice: print_r shows gaps, and the result JSON-encodes as an object. Chain values() as the docs do in every example. Finally, "first wins" means order matters — sort before deduplicating if you care which duplicate survives.
Example
When to use it
- Collapse a merged list of IDs from several sources to a distinct set
- Keep one row per customer after joining several result sets
- Build a distinct tag list from a collection of posts
- Deduplicate on a composite key such as brand plus product type
Frequently asked questions
How do I remove duplicate rows by a column in a Laravel collection?
Pass the column name: $rows->unique('email'). The first row for each distinct value is kept, and chaining ->values() reindexes the result since unique preserves the original keys.
Why does unique() treat 1 and '1' as duplicates?
Because it compares with loose equality by default, so an integer and its string form match. Call uniqueStrict(), or pass true as the second argument, to compare with === instead.
Which duplicate does unique() keep?
The first one it encounters in the collection's current order. Sort the collection before calling unique if you need a specific winner, such as the cheapest quote per supplier.
More examples
Composite uniqueness with a closure
The closure builds a composite key so brand and type together decide uniqueness, giving one row per brand/type pair. The separator matters: joining without one would let 'Apple' + 'watch' collide with 'Applewat' + 'ch' in adversarial data.
<?php
$catalogue = collect([
['name' => 'iPhone 6', 'brand' => 'Apple', 'type' => 'phone'],
['name' => 'iPhone 5', 'brand' => 'Apple', 'type' => 'phone'],
['name' => 'Apple Watch', 'brand' => 'Apple', 'type' => 'watch'],
['name' => 'Galaxy S6', 'brand' => 'Samsung', 'type' => 'phone'],
['name' => 'Galaxy Gear', 'brand' => 'Samsung', 'type' => 'watch'],
]);
$oneEach = $catalogue->unique(fn (array $item) => $item['brand'].'|'.$item['type']);
echo $oneEach->pluck('name')->implode(', '), "\n";
echo $catalogue->unique('brand')->pluck('name')->implode(', '), "\n";
// Output:
// iPhone 6, Apple Watch, Galaxy S6, Galaxy Gear
// iPhone 6, Galaxy S6First wins, so sort first
unique() has no notion of "best" — it keeps the first row it meets for each key, so the input order decides the winner. Sorting beforehand turns that into a deliberate choice. The first output also shows the preserved keys 0 and 2, which is why the second chain ends with values().
<?php
$quotes = collect([
['supplier' => 'Acme', 'price' => 120],
['supplier' => 'Acme', 'price' => 95],
['supplier' => 'Nadir', 'price' => 200],
['supplier' => 'Nadir', 'price' => 210],
]);
// Naive: keeps whichever row happened to come first.
print_r($quotes->unique('supplier')->pluck('price')->all());
// Sort by price first to keep the cheapest per supplier.
print_r($quotes->sortBy('price')->unique('supplier')->pluck('price')->values()->all());
// Output:
// Array ( [0] => 120 [2] => 200 )
// Array ( [0] => 95 [1] => 200 )Loose comparison biting on mixed types
Loose comparison folds '12' into 12 and false into 0, and treats null and '' as equal too, cutting seven values down to four. uniqueStrict keeps all seven because none of them are identical. Whenever a collection mixes types, decide explicitly which behaviour you want rather than taking the default.
<?php
// IDs arriving as a mix of ints and strings, as query input often does.
$ids = collect([12, '12', 13, 0, false, null, '']);
var_dump($ids->unique()->values()->all());
var_dump($ids->uniqueStrict()->values()->all());
// Output:
// array(4) { [0]=> int(12) [1]=> int(13) [2]=> int(0) [3]=> NULL }
// array(7) { [0]=> int(12) [1]=> string(1) "12" [2]=> int(13)
// [3]=> int(0) [4]=> bool(false) [5]=> NULL [6]=> string(0) "" }
Discussion