groupBy — キーやコールバックで要素をグループ化する

collection
  • カテゴリ: collection
  • 掲載バージョン: Laravel 12・PHP 8.4
  • 名前空間 / FQCN: Illuminate\Support\Collection::groupBy
  • 関連: countBy / mapToGroups / partition / keyBy / sortBy
  • 変更履歴: なし

要点(TL;DR)

  • 要素を同じキー(または計算結果)ごとにネストしたコレクションへまとめる
  • 使い方:$users->groupBy('team_id')
  • 罠:
    • 既定でキーは再採番$preserveKeys=false
    • 文字列キーは dot記法user.team.id)で辿れ、見つからないと null グループに入る
    • 多段グループは ['year', 'status']順序がそのままネスト順になる

概要

groupBy は配列/オブジェクトの配列を、指定キー・クロージャ・複数条件で階層的に分類します。Eloquentの取得結果を「月×状態」「部署×職種」などでまとめ、後段で集計・整形に使うのが実務パターンです。クエリビルダの GROUP BY とは別物で、取得後のメモリ内処理です。

構文 / シグネチャ

public function groupBy(string|callable|array $groupBy, bool $preserveKeys = false): Illuminate\Support\Collection
  • 引数(表)
引数必須既定値説明
$groupBy`stringcallablearray`
$preserveKeysboolfalse各グループ内で元のキーを保持するか(false だと0始まりに再採番)
  • 戻り値Collection(キー => Collection)。配列指定時は多重のネストコレクション
  • 例外/副作用
    • コールバック内での例外はそのまま伝播
    • 文字列キーで未定義を辿ると null キーにグループ化
    • LazyCollection でも使用可だが全件を実体化するためメモリ使用量に注意

使用例

最小例

<?php

use Illuminate\Support\Collection;

$items = collect([
    ['type' => 'fruit', 'name' => 'apple'],
    ['type' => 'fruit', 'name' => 'banana'],
    ['type' => 'vegetable', 'name' => 'carrot'],
]);

$grouped = $items->groupBy('type'); // dot記法も可: 'user.team.id'

print_r($grouped->toArray());
// [
//   'fruit' => [
//     ['type' => 'fruit', 'name' => 'apple'],
//     ['type' => 'fruit', 'name' => 'banana'],
//   ],
//   'vegetable' => [
//     ['type' => 'vegetable', 'name' => 'carrot'],
//   ],
// ]

実務例:月×ステータスで売上集計

<?php

use Illuminate\Support\Collection;
use Carbon\Carbon;
use App\Models\Order;

// 例: 今月から過去6ヶ月の注文を取得
$orders = Order::query()
    ->where('created_at', '>=', now()->subMonths(6)->startOfMonth())
    ->get(['id', 'status', 'amount', 'created_at']);

$grouped = $orders->groupBy([
    fn ($o) => $o->created_at->format('Y-m'), // 月
    'status',                                 // ステータス
]);

// 各月×ステータスの件数と売上合計を算出
$summary = $grouped->map(fn ($byMonth) => $byMonth->map(function ($ordersByStatus) {
    return [
        'count' => $ordersByStatus->count(),
        'total_amount' => $ordersByStatus->sum('amount'),
    ];
}));

// 例: 2025-09 の paid 合計
$paidSep = data_get($summary, '2025-09.paid.total_amount', 0);

キー保持(preserveKeys)

$grouped = $items->groupBy('type', preserveKeys: true);
// 各グループ内で、元配列のキーを保持

よくある落とし穴・注意

  • キー再採番$preserveKeys=false では各グループ内が 0,1,2… に再採番。元キーが必要なら true
  • 未定義経路の集約'user.team.id' が存在しない要素は null グループに入る。?? で補正するならコールバックを使う。
  • 多段グループの順序['year','status'] の順がそのまま ネスト順。アクセスは $groups->get($year)->get($status)
  • LazyCollectionの実体化:全件を保持するため大規模データではメモリ圧迫。必要ならDB側で GROUP BY し、結果を小さくしてから groupBy する。
  • ソートは別groupBy は並び替えない。並べたい場合は sortKeys() / sortBy() を合わせて使う。

代替・関連APIとの比較

  • countBy:個数だけ欲しいならこちらが軽量(値→件数)。
  • mapToGroups:コールバックで key => value のペア群を返し、1要素を複数のグループに振り分け可能。groupBy は1層ごとに1キーへ分類
  • partition:真偽で 2分割するだけなら簡潔。
  • keyBy:キーを付け替えるだけでグループ化はしない

コレクション特性(カテゴリ追記)

  • チェーン可:可
  • 破壊的/非破壊:非破壊(新しい Collection を返す)
  • キー保持:オプション($preserveKeys
  • LazyCollection:可(ただし全件実体化
  • 計算量の目安:O(n)(nは要素数)

入出力対応(ミニサンプル)

入力呼び出し出力(概念)
[['t'=>'A'],['t'=>'B'],['t'=>'A']]->groupBy('t')['A'=>[[…],[…]], 'B'=>[[…]]]
[['y'=>2025,'s'=>'paid'], …]->groupBy(['y','s'])[2025=>['paid'=>[[…]], 'fail'=>[[…]]]]

テスト例(Pest)

<?php

use Illuminate\Support\Collection;

it('groups by key and closure', function () {
    $c = collect([
        ['team' => 'A', 'score' => 10],
        ['team' => 'B', 'score' => 20],
        ['team' => 'A', 'score' => 30],
    ]);

    $byTeam = $c->groupBy('team');
    expect($byTeam->keys())->toEqual(collect(['A','B']));
    expect($byTeam['A']->sum('score'))->toBe(40);

    $byHighLow = $c->groupBy(fn($x) => $x['score'] >= 20 ? 'high' : 'low');
    expect($byHighLow['high']->count())->toBe(2);
    expect($byHighLow['low']->count())->toBe(1);
});

トラブルシュート(エラー別)

症状/エラー原因対処
Call to a member function format() on nullcreated_atnulloptional($o->created_at)?->format('Y-m') か nullを別グループに分ける
期待したグループが空dot記法のパス違い / キー名誤りdata_get($item, 'path') で確認、またはクロージャで明示
並びがバラバラgroupBy はソートしない->sortKeys()->map(fn($g)=>$g->sortBy('…')) を併用
メモリ不足大量データをすべて保持DBで GROUP BY → 少量結果に groupBy、もしくは countBy で代替

参考リンク

長野県・北アルプス地方在住のフリーランスWebプログラマー。
「落ち着くためのWeb開発」をテーマに、訪れる人が安心して使えるサービスづくりを心がけています。

LaravelやWordPressなどのWebアプリケーション開発を得意とし、技術面の安定性はもちろん、運用後も長く活用できる設計を大切にしています。
静かな山間の暮らしから生まれる視点で、シンプルかつ本質的な解決策をご提案します。

野鳥観察も趣味のひとつで、特にミソサザイ(Wren)に魅力を感じています。
小さな体に反して力強く上向きの尾羽、そして澄んだ鳴き声が遠くまで響く姿に、静かな存在感と芯の強さを感じます。
このサイト名「Laravel Wren」には、そんなミソサザイのように、小さくても確かな価値を届けたいという想いを込めています。

信頼できるパートナーとして、そして気軽に相談できる存在として、あなたのWebプロジェクトをサポートします。

Yudai Tsuyuzakiをフォローする