メシのタネ

メシのタネになる、Laravelや設計思想の技術配信サイト


Laravel Collection 実践パターン7選


  1. Laravel
  2. Laravel Collection 実践パターン7選

前回はEachだのMapだののながーーい説明を書きましたが、手を動かしたいという方々の要望にお答えし、

便利ではあるけれど、実務で利用するような実例を見てアクティブに勉強していきましょう。

本記事では、Laravel Collectionの中でもmap / filter / reduce / groupBy / chunk / partitionなどを、

組み合わせたり組み合わせなかったりして、実務で使いそうな実践7パターンを紹介します。

なお、CollectionのValueをObjectに縛りをしてるので、より実務に近い形でお送りできるのではないかと感じてます。

で、動くんそれ?そんなあなたに朗報!!

すべてにPestでのテスト付きなので、実際に動かしたりぶっ壊したりしながらド派手に検証しながら学べます。

Pestの環境がない人は、こちらの記事でLaravel12入れてみてください。

語り少なめ、コードもりもりでお送りしていくのでよろしくおねがいします。

🧪 Pattern 1: map × filter × values で整形 + 絞り込み

$users = collect([
    (object)['id' => 1, 'name' => 'kai', 'active' => true],
    (object)['id' => 2, 'name' => 'rei', 'active' => false],
    (object)['id' => 3, 'name' => 'asuka', 'active' => true],
]);

$names = $users
    ->filter(fn($u) => $u->active)
    ->map(fn($u) => $u->name)
    ->values();

目的: 超基本。アクティブなユーザー名だけ抽出。

ポイント: values() を入れないと、元のキーが残って比較しづらくなる。

test('map + filter + values to transform and filter objects', function () {
    $users = collect([
        (object)['id' => 1, 'name' => 'kai', 'active' => true],
        (object)['id' => 2, 'name' => 'rei', 'active' => false],
        (object)['id' => 3, 'name' => 'asuka', 'active' => true],
    ]);

    $names = $users
        ->filter(fn($u) => $u->active)
        ->map(fn($u) => $u->name)
        ->values();

    dump($names->all()); // 確認できるようにつけてます。

    expect($names->all())->toEqual(['kai', 'asuka']);
});
たねまる

valuesしないと番号振り直さないから[0],[2]になっちゃう。values入れると[0][1]になるから安心だね〜

🧪 Pattern 2: intersectByKeys × mapWithKeys で ID制御された抽出

$allowedIds = collect([2 => true, 3 => true]);

$users = collect([
    1 => (object)['id' => 1, 'name' => 'kai'], 
    2 => (object)['id' => 2, 'name' => 'rei'],  
    3 => (object)['id' => 3, 'name' => 'shinji'],
]);

$filtered = $users
    ->intersectByKeys($allowedIds)
    ->mapWithKeys(fn($u) => [$u->id => (object)[
        'id' => $u->id,
        'name' => $u->name,
    ]]);

目的: 外部入力などで与えられたID群に合致するユーザーだけ整形して返す。

ポイント: intersectByKeys は「キーの一致」で制限できるのが強い。

test('intersectByKeys + mapWithKeys for filtered keyed output', function () {
    $allowedIds = collect([
        2 => true,
        3 => true,
    ]);

    $users = collect([
        1 => (object)['id' => 1, 'name' => 'kai'],
        2 => (object)['id' => 2, 'name' => 'rei'],
        3 => (object)['id' => 3, 'name' => 'shinji'],
    ]);

    $filtered = $users
        ->intersectByKeys($allowedIds)
        ->mapWithKeys(fn($u) => [$u->id => (object)[
            'id' => $u->id,
            'name' => $u->name,
        ]]);
    
    dump($filtered->all());

    expect($filtered->keys()->all())->toEqual([2, 3]);
    expect($filtered[2]->name)->toBe('rei');
});
たねまる

この場合idの2と3が指定されるので、id1が落ちるんだね〜

🧪 Pattern 3: reduce で合計を求める

$orders = collect([
    (object)['id' => 1, 'total' => 1200],
    (object)['id' => 2, 'total' => 800],
    (object)['id' => 3, 'total' => 1500],
]);

$sum = $orders->reduce(fn($carry, $order) => $carry + $order->total, 0);

目的: 全ての注文の金額合計を出す。

ポイント: reduceは「畳み込み」。初期値を明示しよう。

test('reduce to calculate total from object values', function () {
    $orders = collect([
        (object)['id' => 1, 'total' => 1200],
        (object)['id' => 2, 'total' => 800],
        (object)['id' => 3, 'total' => 1500],
    ]);

    $sum = $orders->reduce(fn($carry, $order) => $carry + $order->total, 0);

    expect($sum)->toBe(1200 + 800 + 1500);
});
たねまる

この記事書いてるおぢさん、昔Reduceよくわからなかったんだよね〜

🧪 Pattern 4: groupBy × sortBy × keyBy で階層構造のデータを整理

$users = collect([
    (object)['id' => 1, 'name' => 'kai', 'role' => 'admin'],
    (object)['id' => 2, 'name' => 'rei', 'role' => 'user'],
    (object)['id' => 3, 'name' => 'asuka', 'role' => 'user'],
]);

$grouped = $users
    ->groupBy('role')
    ->map(fn($group) => $group
        ->sortBy('name')
        ->keyBy('id')
    );

目的: ロールごとにグループ化、並べ替えて、IDで参照しやすく。

ポイント: map内でさらにCollection操作できるのが強い。

test('groupBy + sortBy + keyBy to structure users by role', function () {
    $users = collect([
        (object)['id' => 1, 'name' => 'kai', 'role' => 'admin'],
        (object)['id' => 2, 'name' => 'rei', 'role' => 'user'],
        (object)['id' => 3, 'name' => 'asuka', 'role' => 'user'],
    ]);

    $grouped = $users
        ->groupBy('role')
        ->map(fn($group) => $group
            ->sortBy('name')
            ->keyBy('id')
        );

    dump($grouped->all());
    
    expect($grouped['user']->keys()->all())->toEqual([3, 2]);
    expect($grouped['admin'][1]->name)->toBe('kai');
});
たねまる

ぐるーぷ分けたくなるよね〜

🧪 Pattern 5: chunk でバッチ処理向けの分割

$users = collect(range(1, 25))->map(fn($i) => (object)['id' => $i]);
$chunks = $users->chunk(10);

目的: 10件ずつ処理したい場合などの分割。

ポイント: chunkはCollectionのCollectionを返す。末尾に余りが入る。

test('chunk splits collection into batches', function () {
    $users = collect(range(1, 25))->map(fn($i) => (object)['id' => $i]);

    $chunks = $users->chunk(10);

    expect($chunks)->toHaveCount(3);
    expect($chunks[0]->count())->toBe(10);
    expect($chunks[1]->count())->toBe(10);
    expect($chunks[2]->count())->toBe(5);
    expect($chunks[2]->pluck('id')->last())->toBe(25);

    // おまけ
    // $[[0]=>[]*10, [1]=>[]*10, [2]=>[]*5]となるので、戻す場合は以下

    $flattened = $chunks->flatten(1); // これで戻る
    expect($flattened)->toHaveCount(25);
    expect($flattened->first()->id)->toBe(1);
    expect($flattened->last()->id)->toBe(25);
});
たねまる

25件のCollectionは10件、10件、5件って分割されていくんだね〜。

🧪 Pattern 6: partition でtrue/falseに分ける

$users = collect([
    (object)['name' => 'kai', 'active' => true],
    (object)['name' => 'rei', 'active' => false],
    (object)['name' => 'asuka', 'active' => true],
]);

[$active, $inactive] = $users->partition(fn($u) => $u->active);

目的: 条件で2分割したいときに最適。

ポイント: true→左、false→右。順番固定なので扱いやすい。

test('partition splits collection by condition', function () {
    $users = collect([
        (object)['name' => 'kai', 'active' => true],
        (object)['name' => 'rei', 'active' => false],
        (object)['name' => 'asuka', 'active' => true],
    ]);

    [$active, $inactive] = $users->partition(fn($u) => $u->active);

    expect($active->pluck('name')->all())->toEqual(['kai', 'asuka']);
    expect($inactive->pluck('name')->all())->toEqual(['rei']);
});

なんでこの、partitonは勝手にtrue/falseで分類してくれるのかというと内部的にこう動いてるからです。

$truthy = [];
$falsy = [];

foreach ($users as $u) {
    if ($u->active) {
        $truthy[] = $u;
    } else {
        $falsy[] = $u;
    }
}

return [collect($truthy), collect($falsy)];

つまり

test('partition truthy vs falsy', function () {
    $numbers = collect([1, 2, 3, 4, 5]);

    [$even, $odd] = $numbers->partition(fn($n) => $n % 2 === 0);

    expect($even->values()->all())->toEqual([2, 4]);
    expect($odd->values()->all())->toEqual([1, 3, 5]);
});

こういうことになっておるわけですね。

たねまる

使える時にオシャレに使ってみてね〜

✅ まとめ:

以下ふりかえり

  • map × filter × values → よくある整形
  • intersectByKeys × mapWithKeys → 入力制限と整形を合体
  • reduce → 合計や集約
  • groupBy → 分類
  • chunk → 分割処理
  • partition → true/falseの世界分割

以上〜。一応最後にテスト全部のっけときます。Eloquentとかでもやってみよっかな。

<?php

test('map + filter + values to transform and filter objects', function () {
    $users = collect([
        (object)['id' => 1, 'name' => 'kai', 'active' => true],
        (object)['id' => 2, 'name' => 'rei', 'active' => false],
        (object)['id' => 3, 'name' => 'asuka', 'active' => true],
    ]);

    $names = $users
        ->filter(fn ($u) => $u->active)
        ->map(fn ($u) => $u->name)
        ->values();

    dump($names->all());

    expect($names->all())->toEqual(['kai', 'asuka']);
});

test('intersectByKeys + mapWithKeys for filtered keyed output', function () {
    $allowedIds = collect([
        2 => true,
        3 => true,
    ]);

    $users = collect([
        1 => (object)['id' => 1, 'name' => 'kai'],
        2 => (object)['id' => 2, 'name' => 'rei'],
        3 => (object)['id' => 3, 'name' => 'shinji'],
    ]);

    $filtered = $users
        ->intersectByKeys($allowedIds)
        ->mapWithKeys(fn($u) => [$u->id => (object)[
            'id' => $u->id,
            'name' => $u->name,
        ]]);
    
    dump($filtered->all());

    expect($filtered->keys()->all())->toEqual([2, 3]);
    expect($filtered[2]->name)->toBe('rei');
});


test('reduce to calculate total from object values', function () {
    $orders = collect([
        (object)['id' => 1, 'total' => 1200],
        (object)['id' => 2, 'total' => 800],
        (object)['id' => 3, 'total' => 1500],
    ]);

    $sum = $orders->reduce(fn($carry, $order) => $carry + $order->total, 0);

    expect($sum)->toBe(1200 + 800 + 1500);
});

test('groupBy + sortBy + keyBy to structure users by role', function () {
    $users = collect([
        (object)['id' => 1, 'name' => 'kai', 'role' => 'admin'],
        (object)['id' => 2, 'name' => 'rei', 'role' => 'user'],
        (object)['id' => 3, 'name' => 'asuka', 'role' => 'user'],
    ]);

    $grouped = $users
        ->groupBy('role')
        ->map(fn($group) => $group
            ->sortBy('name')
            ->keyBy('id')
        );

    dump($grouped->all());
    
    expect($grouped['user']->keys()->all())->toEqual([3, 2]);
    expect($grouped['admin'][1]->name)->toBe('kai');
});


test('chunk splits collection into batches', function () {
    $users = collect(range(1, 25))->map(fn($i) => (object)['id' => $i]);

    $chunks = $users->chunk(10);

    expect($chunks)->toHaveCount(3);
    expect($chunks[0]->count())->toBe(10);
    expect($chunks[1]->count())->toBe(10);
    expect($chunks[2]->count())->toBe(5);
    expect($chunks[2]->pluck('id')->last())->toBe(25);

    // $[[0]=> []*10 , [1]=> []*10 , [2]=> []*5]となるので、戻す場合は以下

    $flattened = $chunks->flatten(1); // これで戻る
    expect($flattened)->toHaveCount(25);
    expect($flattened->first()->id)->toBe(1);
    expect($flattened->last()->id)->toBe(25);
});

test('partition splits collection by condition', function () {
    $users = collect([
        (object)['name' => 'kai', 'active' => true],
        (object)['name' => 'rei', 'active' => false],
        (object)['name' => 'asuka', 'active' => true],
    ]);

    [$active, $inactive] = $users->partition(fn($u) => $u->active);

    expect($active->pluck('name')->all())->toEqual(['kai', 'asuka']);
    expect($inactive->pluck('name')->all())->toEqual(['rei']);
});


test('partition truthy vs falsy', function () {
    $numbers = collect([1, 2, 3, 4, 5]);

    [$even, $odd] = $numbers->partition(fn($n) => $n % 2 === 0);

    expect($even->values()->all())->toEqual([2, 4]);
    expect($odd->values()->all())->toEqual([1, 3, 5]);
});

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

This site uses Akismet to reduce spam. Learn how your comment data is processed.