Collection::groupBy

Groups a collection's items into sub-collections keyed by a field or callback result.

Signature
Collection::groupBy(array|callable|string $groupBy, bool $preserveKeys = false): static

Parameters

ParameterTypeDescription
$groupBy(callable(TValue, TKey): TGroupKey)|array|stringA 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.
$preserveKeysoptionalbool= falseKeep 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

Try it yourself
Loading editor…
Press Run to execute the code.

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.

Example · php
<?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.

Example · php
<?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.

Example · php
<?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

  • Be the first to comment on this lesson.