Collection::groupBy
Groups a collection's items into sub-collections keyed by a field or callback result.
Collection::groupBy(array|callable|string $groupBy, bool $preserveKeys = false): staticParameters
| Parameter | Type | Description |
|---|---|---|
$groupBy | (callable(TValue, TKey): TGroupKey)|array|string | A field name to read from each item, a callback returning the group key, or an array of either for nested grouping. A callback that returns an array places the item into several groups at once. |
$preserveKeysoptional | bool= false | Keep each item's original collection key inside its group instead of re-indexing from 0. |
Return value
staticA collection keyed by each distinct group value, where every value is itself a Collection of the matching items. Passing an array of criteria produces one nesting level per criterion.
groupBy buckets items by a field or by whatever a callback returns. The outer collection is keyed by each distinct group value; every value inside it is itself a Collection of the matching items.
When to reach for it
Use it to shape data for display — orders by month, tasks by status, rows by owner — when a single query has already fetched everything you need. Passing an array of criteria builds nested groups, one level per criterion, which saves writing the same loop twice. A callback that returns an array of keys puts the item into several groups at once, which is how you group by a many-valued field such as roles or tags.
The thing that trips people up
The values are Collection objects, not arrays. $grouped->all() gives you an array of collections, which prints as a wall of objects and breaks code expecting arrays; use toArray() when you want the whole structure converted, or map->all() for one level.
Items are re-indexed inside their group unless you pass preserveKeys: true — a real problem when the original keys were model ids. And the group keys come out in first-seen order, not sorted, so add sortKeys() if the display order matters. Non-string keys are coerced: booleans become 0 and 1, null becomes an empty string, and enums become their value.
Example
When to use it
- Bucket already-fetched orders by month to render a summary table.
- Group tasks by status and count each bucket for a dashboard.
- Build a two-level breakdown, such as region then product, in a single call.
Frequently asked questions
Why does groupBy return objects instead of arrays?
Each group is a Collection instance so you can chain methods on it directly. Call toArray() on the outer collection to convert the whole nested structure, or map->all() to convert just one level.
How do I keep the original keys inside each group?
Pass true as the second argument, ideally as the named argument preserveKeys: true. Without it every group is re-indexed from 0, which loses model ids or other meaningful keys.
Can I group by more than one field?
Yes — pass an array of field names or callbacks. Each element creates one further level of nesting, and a callback that returns an array places the item into several groups on that level at once.
More examples
The groups are collections, not arrays
The loop prints "red: Ada, Cleo" then "blue: Bob", and the final line prints ['red' => 2, 'blue' => 1]. Because the buckets are Collections, pluck, implode and count work inside them without any conversion — and the higher-order map->count() proxy counts every group in one expression.
<?php
$rows = collect([
['team' => 'red', 'name' => 'Ada'],
['team' => 'blue', 'name' => 'Bob'],
['team' => 'red', 'name' => 'Cleo'],
]);
$grouped = $rows->groupBy('team');
// Each value is a Collection, so collection methods chain straight through.
foreach ($grouped as $team => $members) {
echo $team, ': ', $members->pluck('name')->implode(', '), PHP_EOL;
}
print_r($grouped->map->count()->all());Nested grouping and multi-valued keys
The outputs are [1, 2], then [Role_1, Role_3, Role_2], then [10, 20]. Two things to note: the callback returns an array, so user 1 lands in both Role_1 and Role_3, and the role keys appear in first-seen order rather than sorted. With preserveKeys the innermost bucket still carries the original ids 10 and 20.
<?php
$data = collect([
10 => ['user' => 1, 'skill' => 1, 'roles' => ['Role_1', 'Role_3']],
20 => ['user' => 2, 'skill' => 1, 'roles' => ['Role_1', 'Role_2']],
30 => ['user' => 3, 'skill' => 2, 'roles' => ['Role_1']],
]);
$result = $data->groupBy(['skill', fn (array $item) => $item['roles']], preserveKeys: true);
print_r($result->keys()->all());
print_r($result[1]->keys()->all());
print_r($result[1]['Role_1']->keys()->all());Grouping models by date
Formatting the Carbon instance inside the callback gives a stable, sortable group key, and sortKeys() puts the months in chronological order — groupBy itself preserves first-seen order, which follows whatever the query returned. This snippet needs a booted application with Eloquent, so it is shown rather than executed.
<?php
$byMonth = $orders->groupBy(fn ($order) => $order->created_at->format('Y-m'))
->sortKeys();
foreach ($byMonth as $month => $group) {
echo $month, ': ', $group->sum('total'), PHP_EOL;
}
Discussion