- カテゴリ: collection
- 掲載バージョン: Laravel 12・PHP 8.4
- 名前空間 / FQCN:
Illuminate\Support\Collection::groupBy - 関連:
countBy/mapToGroups/partition/keyBy/sortBy - 変更履歴: なし
LaravelのgroupByは2種類ある
Laravelで「groupBy」と検索すると、2つの異なるgroupByがヒットします。混同しやすいため、最初に整理しておきます。
Collection groupBy() | Query Builder / Eloquent groupBy() | |
|---|---|---|
| 処理場所 | PHP(メモリ上) | MySQL / PostgreSQL 等のDB |
| 対象 | 取得済みのコレクション | DBテーブルの行 |
| 実行タイミング | get() 等で取得した後 | SQLを生成するとき |
| 戻り値 | Collection(キー => Collection) | クエリ結果(レコード集合) |
| 集計 | count() / sum() を後からPHPで実行 | COUNT() / SUM() をDBが実行 |
| 大量データ | 全件をメモリに保持するため注意 | DB側で集計するため効率的 |
| 典型的な使い方 | $users->groupBy('role') | DB::table('orders')->groupBy('status')->get() |
このページでは Collection の groupBy()(PHPのメモリ上で動作)を解説します。SQL の GROUP BY を使いたい場合は Query Builder の groupBy() を参照してください。
要点(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 | `string | callable | array` | ✓ |
| $preserveKeys | bool | false | 各グループ内で元のキーを保持するか(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);
// 各グループ内で、元配列のキーを保持
map() / pluck() / count() との組み合わせ
<?php
use Illuminate\Support\Collection;
$orders = collect([
['status' => 'paid', 'amount' => 1000],
['status' => 'pending', 'amount' => 500],
['status' => 'paid', 'amount' => 2000],
['status' => 'failed', 'amount' => 300],
['status' => 'pending', 'amount' => 700],
]);
$grouped = $orders->groupBy('status');
// count(): ステータスごとの件数
$counts = $grouped->map(fn ($g) => $g->count());
// ['paid' => 2, 'pending' => 2, 'failed' => 1]
// sum(): ステータスごとの合計金額
$totals = $grouped->map(fn ($g) => $g->sum('amount'));
// ['paid' => 3000, 'pending' => 1200, 'failed' => 300]
// pluck(): ステータスごとにamountの配列だけ抜き出す
$amounts = $grouped->map(fn ($g) => $g->pluck('amount'));
// ['paid' => [1000, 2000], 'pending' => [500, 700], 'failed' => [300]]
よくある落とし穴・注意
- キー再採番:
$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()を合わせて使う。 - SQLの GROUP BY と混同する:Query Builder の
->groupBy('status')はSQLのGROUP BYを生成します。Collection のgroupBy()とはまったく別の処理です。SQLのGROUP BYは集計関数(COUNT/SUM等)と組み合わせないとエラーになりますが、Collection のgroupBy()は全レコードをグループ別に保持します。大量データの集計はSQL側 (->selectRaw('status, COUNT(*) as cnt')->groupBy('status')->get()) が効率的です。 - 戻り値のネスト構造を把握する:
groupBy()の戻り値はCollection<string, Collection>です。$grouped['paid']でアクセスした値はさらにCollectionなので$grouped['paid'][0]['amount']のように配列ライクにアクセスできます。多段グループ(['year', 'status'])の場合は$grouped->get('2025-09')->get('paid')と2段アクセスが必要です。dd($grouped->toArray())でネスト構造を確認するとデバッグしやすいです。
代替・関連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 null | created_at が null | optional($o->created_at)?->format('Y-m') か nullを別グループに分ける |
| 期待したグループが空 | dot記法のパス違い / キー名誤り | data_get($item, 'path') で確認、またはクロージャで明示 |
| 並びがバラバラ | groupBy はソートしない | ->sortKeys() や ->map(fn($g)=>$g->sortBy('…')) を併用 |
| メモリ不足 | 大量データをすべて保持 | DBで GROUP BY → 少量結果に groupBy、もしくは countBy で代替 |
FAQ
LaravelのgroupByとは?
Laravelの groupBy() とは、コレクション内の要素を指定キーや条件でグループ分けするメソッドです。戻り値は「グループキー => Collection」の形式になり、各グループをさらに count() や sum() で集計できます。例えば「ユーザーを役職別にまとめる」「注文をステータス別・月別に分類する」といった用途に使います。なお、同名のメソッドがQuery Builderにも存在しますが、それはSQL側のGROUP BYであり別物です。
CollectionとSQLのgroupByは違う?
はい、まったく異なります。SQLの GROUP BY(Query Builderの ->groupBy())はデータベース側で集計を行い、集計関数(COUNT / SUM 等)と一緒に使います。一方、Collectionの groupBy() はPHP上でコレクションを分類するだけで、すべての要素をメモリ上に保持します。大量データの集計にはSQL側のGROUP BYが適しており、取得後の二次整理にCollectionのgroupByが向いています。
複数条件でグループ化できる?
はい、配列で複数のキーを渡すことで多段グループ化が可能です。
<?php
// ['year', 'status'] の順にネストされる
$grouped = $orders->groupBy(['year', 'status']);
// アクセス例(2段 get())
$paidIn2025 = $grouped->get('2025')->get('paid');
// クロージャと文字列を混在させることも可能
$grouped2 = $orders->groupBy([
fn ($o) => $o->created_at->format('Y'), // 年(クロージャ)
'status', // ステータス(文字列キー)
]);
配列の先頭から順に外側(第1階層)→内側(第2階層)とネストしていきます。アクセスは $grouped->get($year)->get($status) のように2段で行います。
参考リンク
- Laravel Docs — Collections: groupBy(公式): https://laravel.com/docs/12.x/collections#method-groupby
- ソースコード(Illuminate\Support\Collection): https://github.com/laravel/framework/blob/12.x/src/Illuminate/Support/Collection.php
- ヘルパ
data_get(dot記法): https://laravel.com/docs/12.x/helpers#method-data-get - LazyCollection 概要: https://laravel.com/docs/12.x/collections#lazy-collections
