Laravel

Laravel Collectionに値を追加する方法と検索|push・merge・contains・firstWhereの挙動差【13.x】

Laravel Collectionに値を追加する方法と検索|push・merge・contains・firstWhereの挙動差【13.x】

Laravel の Collection に値を足すメソッドは主なものだけで8つあり、元のコレクションを書き換えるものと新しいインスタンスを返すものに分かれます。この違いを知らずに merge を書くと、実行しても値は増えません。Eloquent のクエリ結果に対する merge はさらに別の動きをし、主キーが同じ要素を黙って上書きします。この記事では laravel/framework 13.38.0 を PHP 8.5.8 で実際に動かした結果をもとに、追加系メソッドの使い分け、各要素にキーを足す方法、contains・has・search・firstWhere の違いまでを整理します。

まとめ

  • push add put prepend unshift は元のコレクションを書き換える。merge concat union は新しいインスタンスを返すので、戻り値を受け取らないと何も起きない
  • merge は文字列キーを上書きし数値キーを振り直す。concat は渡した側のキーを捨てて末尾に連番で足す
  • 各要素にキーを足すなら map か transform。配列の要素に each の中で代入しても元のコレクションには反映されない
  • Eloquent Collection の merge は主キーで要素を突き合わせる。別のモデルでも ID が同じなら上書きされ、件数が減る。混ぜるなら concat
  • contains は値の有無、has はキーの有無、search は値が見つかったキーを返す。search の戻り値 0 は if で偽になる
  • firstWhere にデフォルト値の引数はなく、未ヒットなら null。代替値は first の第2引数、例外にしたいなら firstOrFail
  • filter sortBy where は元のキーを残す。JSON 配列として返したい場合は values() でキーを振り直す

Collectionに値を追加する主なメソッドと使い分け

末尾に足すのか、キーを指定するのか、別のコレクションと結合するのかで使うメソッドが変わります。まずは元のコレクションを書き換えるかどうかで分けて押さえます。

破壊的メソッドと非破壊メソッドの一覧

メソッド 元のコレクション 戻り値 追加位置とキー
push(…$values) 書き換える 自分自身 末尾・連番(複数可)
add($item) 書き換える 自分自身 末尾・連番(1つだけ)
put($key, $value) 書き換える 自分自身 キー指定・既存なら上書き
prepend($value, $key = null) 書き換える 自分自身 先頭・キー指定も可
unshift(…$values) 書き換える 自分自身 先頭・連番(複数可)
merge($items) 変えない 新インスタンス 文字列キーは上書き
concat($source) 変えない 新インスタンス 末尾・キーは捨てる
union($items) 変えない 新インスタンス 既存キーを優先

v13.38.0 の Collection.php では、上5つが $this->items を直接操作して $this を返し、merge concat union は newInstance() で組み立てた別のコレクションを返します。

push・add・put・prependの書き分け

$c = collect(['a', 'b']);

$c->push('c', 'd');      // 末尾に複数まとめて追加
$c->add('e');            // 末尾に1つだけ追加
$c->put('k', 'v');       // キーを指定して追加・既存キーなら上書き
$c->prepend('z');        // 先頭に追加
$c->unshift('y1', 'y2'); // 先頭に複数まとめて追加

$c->all();
// [0 => 'y1', 1 => 'y2', 2 => 'z', 3 => 'a', 4 => 'b', 5 => 'c', 6 => 'd', 7 => 'e', 'k' => 'v']

add はソースに存在しますが、公式ドキュメントのコレクション章には項目がありません。push は可変長引数なので、末尾追加は push に寄せておくと、後で複数追加へ変えても書き換えが要りません。先頭側は1件なら prepend、複数なら unshift です。

merge・concat・unionのキー衝突時の挙動差

merge は PHP の array_merge をそのまま使うので、文字列キーが衝突すると後から渡した値で上書きされます。concat は元のコレクションを複製し、渡された要素を1つずつ push するだけなのでキー名を見ません。

$base = collect(['name' => 'Desk', 'price' => 100]);

$base->merge(['price' => 200])->all();
// ['name' => 'Desk', 'price' => 200]

$base->concat(['price' => 200])->all();
// ['name' => 'Desk', 'price' => 100, 0 => 200]

$base->union(['price' => 200, 'stock' => 3])->all();
// ['name' => 'Desk', 'price' => 100, 'stock' => 3]

collect([1, 2, 3])->merge([3, 4])->all();   // [1, 2, 3, 3, 4]
collect([1, 2])->union([9, 8, 7])->all();   // [1, 2, 7]

union の実体は PHP の + 演算子で、左辺にあるキーを優先して残します。最後の例でキー 0 と 1 の 9 と 8 が捨てられ 7 だけが残るのはこのためです。値ではなくキーで重複を判定するので、値の重複排除には使えません。重複を消すなら merge の後に unique() を挟みます。設定値の上書きなら merge、ログのような追記なら concat、既定値を後から補うだけなら union と選ぶのが実装の意図に沿います。

非破壊メソッドの戻り値を捨てたときの症状

$items = collect(['a']);

foreach ($rows as $row) {
    $items->merge([$row]);            // 何も起きない
}

foreach ($rows as $row) {
    $items = $items->merge([$row]);   // 代入すれば増える
    // あるいは $items->push($row);
}

エラーも警告も出ないため、ループを抜けた後に件数が合わずに気づくことになります。merge concat union map filter sortBy pluck only はどれも新しいインスタンスを返す側です。挙動を手元で確かめるなら、php artisan tinkerの使い方|Laravel Tinkerの基本コマンドとEloquent操作の手順で $c->merge([4]) の後に $c を評価すれば、戻り値と元の変数の差が1行で見えます。

各要素に値を追加する:map・transform・eachの使い分け

「コレクションに値を追加する」には、要素を1つ増やす意味のほかに、すべての要素に税額やラベルのようなキーを1つずつ足す意味もあります。後者は push や put では書けず、map か transform を使います。

$rows = collect([
    ['id' => 1, 'price' => 100],
    ['id' => 2, 'price' => 250],
]);

$rows->each(function ($row) {
    $row['tax'] = $row['price'] * 0.1;   // 反映されない
});

$withTax = $rows->map(fn ($r) => [...$r, 'tax' => (int) ($r['price'] * 0.1)]);
// [['id' => 1, 'price' => 100, 'tax' => 10], ['id' => 2, 'price' => 250, 'tax' => 25]]
// $rows は元のまま

$rows->transform(fn ($r) => $r + ['tax' => (int) ($r['price'] * 0.1)]);
// $rows 自体が tax 付きに置き換わる

each で代入しても反映されないのは、PHP の配列がクロージャへ値渡しされ、書き換えたのはコピーだからです。要素がオブジェクトなら同じオブジェクトを指す識別子が渡るので、$o->flag = true のような代入は元のコレクションにも効きます。Eloquent モデルも同じで、$posts->each(fn ($p) => $p->label = '#' . $p->id) の後に toArray() すると label が出力に含まれました。

選び分けは単純です。配列要素の元を残して加工版を別に持つなら map、元のコレクションの要素を置き換えてよいなら transform、要素がモデルやオブジェクトでその場で属性を足すだけなら each で構いません。配列の要素に each で代入するコードは、動いているように見えて何もしていないので書かないでください。

Eloquent Collectionのmergeによる主キー衝突と要素の上書き

クエリ結果の Illuminate\Database\Eloquent\Collection は merge を上書きしており、要素を主キーで辞書にしてから突き合わせます。v13.38.0 の Eloquent\Collection.php の実装は、渡された要素ごとに getKey() を取り、同じキーがあれば差し替え、最後に array_values() で詰め直すものです。モデルのクラスやテーブルは見ていません。

$posts  = Post::all();    // id 1, 2
$videos = Video::all();   // id 1, 2, 3

$posts->merge($videos)->count();            // 3(Post は2件とも消え、中身は Video だけ)
$posts->concat($videos)->count();           // 5
$posts->toBase()->merge($videos)->count();  // 5

Post::whereKey(1)->get()->merge(Post::all())->count();  // 2(同じ id は1件に)
$posts->merge([new Post(['title' => 'draft'])])->count(); // 2(主キーが null のモデルは追加されない)
$posts->merge([['id' => 9]]);  // Error: Call to a member function getKey() on array

上の結果は、SQLite のメモリ DB に posts と videos を作り、13.38.0 で実行したものです。投稿と動画を新着順に並べるタイムラインのように、別テーブルのモデルを1つのリストにまとめる処理で踏みます。ID が重なった分だけ黙って欠け、例外は出ません。公式の Eloquent コレクション章は Eloquent 側で追加・変更されたメソッドとして contains や find などを挙げていますが、merge は載っていません。

判断の基準はこうです。同じモデルの検索結果を重複なく足し合わせたいなら Eloquent の merge は便利で、そのまま使ってよい。別のモデルを混ぜる、主キーが null の未保存モデルを含める、件数を保ちたい、のどれかに当てはまるなら concat を使うか、toBase() で Support Collection に落としてから merge します。並べ替えは結合の後に sortByDesc(‘created_at’) で行います。Eloquent モデルやリレーションの基礎はLaravel Eloquentとは|モデル定義・リレーション・eager loadingの基礎とクエリビルダの使い分けで整理しています。

Collectionの基礎:配列との違いと2種類のCollection

Collection は Illuminate\Support\Collection のインスタンスで、内部に配列を $items として持ちます。生の配列との違いは、array_map や usort を入れ子で書く代わりにメソッドチェーンで書ける点と、Eloquent のクエリ結果がそのまま Collection で返る点です。

collect関数とCollectionクラスからの生成

$c = collect([1, 2, 3]);
$c = collect(['name' => 'Desk', 'price' => 100]);
$c = new Illuminate\Support\Collection([1, 2, 3]);
$c = Illuminate\Support\Collection::make([1, 2, 3]);

collect() は内部で new Collection() を呼ぶだけで、どの書き方でも同じコンストラクタを通ります。null とスカラー値は Arr::wrap() を通るため、collect(null) は空、collect('abc') は要素1つのコレクションになります。オブジェクトは Arr::from() が型で分岐し、Arrayable Jsonable JsonSerializable Traversable を実装していればその変換結果を要素に使います。どれにも当たらない通常のオブジェクトは (array) キャストされ、public だけでなく protected・private のプロパティも要素に入ります。protected と private のキーには NUL 文字で囲んだ接頭辞が付くので、DTO をそのまま collect() に渡すのは避けてください。

Support CollectionとEloquent Collectionの違い

User::all() が返す Eloquent Collection は Support Collection のサブクラスで、contains find load modelKeys merge などがモデル前提に上書きされています。戻り値の型にも差があり、Eloquent Collection の pluck は常に toBase() を通って Support Collection に落ちます。map は結果にモデル以外が1つでも混じったときだけ落ちます。チェーンの途中でこれらを挟むと、後段の load() が呼べなくなります。モデルの一括生成を避けたい場合は、cursor() が返す LazyCollection を使えます。ただし PDO の結果バッファでメモリを消費するため、非常に大量のデータやリレーションの eager loading が必要な場合は lazy() も検討します。

要素の存在確認:contains・has・searchの違い

「含まれるか」を調べるメソッドは、値を見る contains、キーを見る has、値が見つかったキーを返す search の3つで、取り違えると条件分岐が逆になります。なお「laravel contains」で文字列の部分一致を探しているなら、コレクションではなく Str::contains($haystack, $needles, $ignoreCase = false) が該当します。collect(['Laravel Collection'])->contains('Laravel') は要素の完全一致を見るので false です。

containsの緩い比較とcontainsStrictの型判定

collect([1, 2, 3])->contains('1');         // true('1' == 1)
collect([1, 2, 3])->containsStrict('1');   // false
collect(['a', 'b'])->contains(0);          // false(PHP 8 の比較規則)

$items = collect([
    ['name' => 'Desk',  'price' => 200],
    ['name' => 'Chair', 'price' => 100],
]);
$items->contains('name', 'Desk');      // true(行の配列に対するキーと値)
$items->contains('price', '>', 150);   // true(演算子を挟める)
$items->contains(fn ($v) => $v['price'] < 50);  // false

collect(['name' => 'Desk'])->contains('name', 'Desk');  // false
collect(['name' => 'Desk'])->contains('Desk');          // true

値を1つだけ渡すと、内部は in_array($key, $this->items) で、in_array の第3引数を付けない緩い比較です。引数が2つ以上なら where と同じ比較関数が作られ、各要素から data_get($item, $key) で取った値を比べます。そのため各要素から指定キーの値を取得できる必要があります。行の配列だけでなくオブジェクトや Eloquent モデルにも使えますが、連想配列1つだけのコレクションに contains('name', 'Desk') を渡すと、各要素は文字列なので false になります。

リクエストパラメータは文字列で届くので、$request->input('id') をそのまま contains に渡すと数値 ID に一致します。権限チェックで整数 ID を比較するなら、入力が許容する形式と範囲の整数であることを検証してから int に変換し、整数 ID のコレクションに対して containsStrict を使ってください。Laravel 13 は composer.json で PHP 8.3 以上を要求しており、PHP 7 系で true だった in_array(0, ['a', 'b']) は false になります。

キーを調べるhas・位置を返すsearchとの違い

$c = collect(['name' => 'Desk', 'price' => 100]);

$c->has('name');        // true(キー name がある)
$c->contains('name');   // false(値 'name' は無い)
$c->has('Desk');        // false
$c->contains('Desk');   // true

collect(['a' => null])->has('a');   // true(値が null でもキーがあれば真)

collect(['x', 'y'])->search('x');   // 0
collect(['x', 'y'])->search('z');   // false
if (collect(['x', 'y'])->search('x')) { }   // 0 なので通らない

has は array_key_exists で判定するので、値が null のキーにも true を返します。search は array_search と同じく見つかったキーを返します。0 始まりの連番キーなら先頭要素のキーは 0 ですが、文字列キーや欠番を含む場合は元のキーが返ります。有無だけを知りたいなら contains を使い、search を使うなら !== false で判定します。第2引数に true を渡すと厳密比較になり、collect([1, 2])->search('1', true) は false です。

Eloquent Collectionのcontainsにおける主キー判定

$users = User::all();

$users->contains(1);              // id が 1 のユーザーがいるか(== で比較)
$users->contains('1');            // true(文字列でも一致)
$users->contains(User::find(1));  // is() で同一モデルか
$posts->contains(Video::find(1)); // false(id が同じでもテーブルが違う)

Eloquent 側は、引数が1つでモデルなら $model->is($key)、それ以外なら $model->getKey() == $key で比べます。is() は主キー・テーブル名・接続名の3つを === で比べるので、モデルを渡す形は厳密、素の値を渡す形は緩い、という非対称になります。前章の merge が ID だけで上書きするのに対し、contains にモデルを渡せばテーブルまで区別される点も対照的です。

条件に一致する要素の取得:firstWhere・first・firstOrFail・sole

firstWhere は条件に一致する最初の1件を返します。実装は first() に条件クロージャを渡しているだけで、同じクロージャを filter に渡してコレクションを返すのが where です。

firstWhereにデフォルト値引数が無い場合の代替

$users = collect([
    ['name' => 'Sato',   'age' => 30, 'admin' => false],
    ['name' => 'Suzuki', 'age' => 40, 'admin' => true],
]);

$users->firstWhere('age', 40);        // ['name' => 'Suzuki', ...]
$users->firstWhere('age', '>=', 35);  // 演算子を第2引数に挟める
$users->firstWhere('age', 99);        // null

$users->first(fn ($u) => $u['age'] === 99, ['name' => 'guest']);  // 未ヒット時は第2引数
$users->firstWhere('age', 99) ?? ['name' => 'guest'];             // 同じ意味
$users->firstOrFail('age', 99);  // ItemNotFoundException

シグネチャは firstWhere($key, $operator = null, $value = null) で、デフォルト値を受ける引数がありません。代替値が要るなら first の第2引数か ??、見つからないこと自体を異常として扱うなら firstOrFail にします。null のプロパティを読み取ると PHP では警告になり、Laravel の通常のエラーハンドラでは例外に変換されます。未ヒット時は、代替値・例外・null 判定による分岐のいずれかで扱ってください。

引数1つのfirstWhereと比較の緩さ

$users->firstWhere('admin') のようにキーだけを渡すと、内部で値 true と演算子 = が補われます。「admin キーを持つ最初の要素」ではなく「admin が真と評価される最初の要素」で、false や 0 の要素は飛ばされます。キーの値だけが欲しいなら value('admin') で、ドット記法のネストキーにも効きます。

2引数の比較は == なので、firstWhere('age', '40') も整数 40 に一致します。厳密に比べるなら firstWhere('age', '===', 40) か whereStrict('age', 40)->first() です。比較の前に enum_value() が通るので、enum キャストの列を firstWhere('status', Status::Active) と backed enum で探すと、文字列 ‘active’ の要素に一致します。値を enum の定義に揃える方法はLaravelのmigrationでenumを扱う実践ガイド|カラム定義・値の追加・Eloquent連携【Laravel 13対応】にまとめています。

Model::firstWhereとCollection::firstWhereの違い

User::firstWhere('email', '[email protected]');
// select * from "users" where "email" = ? limit 1

User::all()->firstWhere('email', '[email protected]');
// select * from "users"(全件を取ってから PHP 側で探す)

同じ名前でも、モデルやクエリビルダから呼ぶ firstWhere は where(...)->first() を SQL で実行し、1件だけ取ります。クエリログで確かめると、Collection 側は WHERE も LIMIT も付かない全件取得でした。DB から1件探すだけなら Model::firstWhere を使い、取得済みのコレクションを何度も引くときだけ Collection の firstWhere を使います。未ヒットはどちらも null です。

1件だけか複数あるかの判定:sole・hasSole・hasMany

$users->sole('age', 40);      // 一致がちょうど1件ならその要素
                              // 0件は ItemNotFoundException、2件以上は MultipleItemsFoundException
$users->hasSole('age', 30);   // true(一致がちょうど1件)
$users->hasMany('age', 40);   // 一致が2件以上なら true

hasSole は v12.49.0(2026-01-28)、hasMany は v12.50.0(2026-02-04)で追加されました。同じ役割だった containsOneItem と containsManyItems は、ソースの docblock でそれぞれ 12.49.0 と 12.50.0 から非推奨とされ、13.x の公式ドキュメントからは項目が消えています。今も動きますが、新しく書くなら hasSole と hasMany です。

集計と取り出し:reduce・pluck・sortBy・mapの引数と戻り値

ここからは値を取り出して形を変える系のメソッドで、どれも元のコレクションは変えません。

reduceコールバックの第3引数と初期値の既定

$total = collect(['a' => 100, 'b' => 200])
    ->reduce(fn ($carry, $value, $key) => $carry + $value, 0);
// 300。第2引数の 0 が初期値

collect([1, 2, 3])->reduce(fn ($carry, $v) => $carry + $v);
// 初期値を省略すると null から始まる

コールバックは ($result, $value, $key) の順でキーまで受け取れるので、キー名で加算対象を絞る集計を filter なしで書けます。初期値の既定は null で、数値の加算なら 0 扱いになりますが、文字列の連結では空文字を明示してください。単純な合計や平均は sum() avg() の方が意図が伝わります。

pluck・sortBy・mapの引数順とドット記法

$posts->pluck('author.name');      // ネストした値を取り出す
$posts->pluck('title', 'id');      // 第1引数が値、第2引数がキー
$posts->sortBy('author.name');     // 並べ替えのキーにも使える
$posts->sortByDesc('published_at');

pluck は「値、キー」の順で、逆に書くと値とキーが入れ替わります。sortBy は第2引数にソートフラグ(SORT_NATURAL など)、第3引数に降順フラグを取り、配列で複数キーを渡すと内部で sortByMany に振り分けられます。Eloquent Collection に map を使い、配列を返すと Support Collection に、モデルを返すと Eloquent Collection のままになることは 13.38.0 で確かめました。

キー保持がAPIレスポンスを壊す:values()を挟む判断基準

filter where sortBy reject は元の要素のキーをそのまま残します。filter はコールバックを渡すと Arr::where() を経由し、その実体は array_filter(..., ARRAY_FILTER_USE_BOTH) なのでキーは飛び飛びになります。

collect([1, 2, 3, 4])->filter(fn ($v) => $v > 1)->toJson();
// {"1":2,"2":3,"3":4} ← 配列ではなくオブジェクト

collect([1, 2, 3, 4])->filter(fn ($v) => $v < 4)->toJson();
// [1,2,3] ← この例は末尾だけを除外するため、キーが0から連続する

collect([1, 2, 3, 4])->filter(fn ($v) => $v > 1)->values()->toJson();
// [2,3,4]

json_encode はキーが 0 から連続する配列だけを JSON の配列として出し、欠番があればオブジェクトにします。2つ目の例では末尾だけを除外するため、残るキーが 0 から連続し、JSON 配列になります。先頭を残しても途中の要素を除外すると欠番ができ、JSON オブジェクトになります。データの中身で出力の型が変わるため、テストデータでは通り、本番データで初めてフロントの map() が落ちます。

結果を JSON 配列として返す場合や、foreach で 0 始まりの連番を必要とする場合は、filter や sortBy の後に values() でキーを振り直します。逆に ID をキーにした連想配列として引き回している場合や、keyBy() で意図的にキーを張り直した後に values() を呼ぶとキーが消えます。API リソースでレスポンスを組み立てる流れはLaravel APIの作り方|install:apiとAPIリソース・エラー応答の統一で扱っています。こうした挙動の差を設計段階で潰した Laravel の業務システムを一から組みたい場合は、フルスクラッチ開発としてご相談いただけます。

よくある質問

Laravel Collectionに値を追加するとき、pushとmergeのどちらを使うべきですか?

末尾に要素を足すだけなら push です。元のコレクションを直接書き換えるので、ループで結果を積み上げる用途に向きます。merge は新しいインスタンスを返すので、戻り値を必ず受け取ります。既存のキーを上書きしたいなら merge、キーを無視して末尾に連結したいなら concat です。

コレクションの各要素に新しいキーを追加するにはどうしますか?

元を残すなら map で新しい配列を返し、元の変数を置き換えてよいなら transform を使います。要素が配列のとき each の中で代入しても、コピーを書き換えるだけで元には反映されません。要素が Eloquent モデルなら each で属性を代入しても反映されます。

Eloquentのコレクションをmergeすると件数が減るのはなぜですか?

Eloquent Collection の merge は要素を主キーで突き合わせ、同じキーなら差し替えるためです。別のモデルでも ID が同じなら上書きされます。また、merge に渡したモデルの主キーが null なら追加されません。未保存でも主キーを設定済みなら結合対象になります。件数を保ちたいなら concat を使うか、toBase() で Support Collection にしてから merge します。

containsとhasの違いは何ですか?

contains は値の有無、has はキーの有無を調べます。collect(['name' => 'Desk']) では has('name') と contains('Desk') が true で、contains('name') は false です。値が見つかった位置が欲しいなら search を使い、戻り値の 0 と false を !== false で区別します。

firstWhereがnullを返すとき、デフォルト値を指定できますか?

firstWhere にデフォルト値の引数はないので、直接は指定できません。$c->first(fn ($v) => $v['age'] === 40, $default) と書くか、$c->firstWhere('age', 40) ?? $default で受けます。見つからないことを例外にしたいなら firstOrFail を使います。

filterの結果をJSONで返すと配列にならないのはなぜですか?

filter が元のキーを残すためです。先頭要素が除外されるとキーが 1 から始まり、json_encode は 0 から連続しない配列をオブジェクトとして出します。API のレスポンスに使うなら values() でキーを振り直してください。

関連記事

お気に入りに入れた記事の一覧

一創の公式 SNS

X では毎朝の新着記事と IT ニュースの速報を、YouTube・TikTok・Instagram・LinkedIn・Facebook では記事の解説動画を配信しています。

この記事は以下の記事からリンクされています

資料請求

今日のトレンド記事 直近 24 時間で、いつもより多く読まれている記事

  1. 2026.10.29 テックブログ ソニー生命の不正アクセス:38.5万件の対象確認と窓口、停止中の手続き
  2. 2026.10.31 テックブログ 松屋の不正アクセス:237万件の対象確認と窓口、漏れたパスワードの守り方
  3. 2026.10.31 テックブログ 無印良品の情報漏洩:MUJIアプリ最大2,726万件の対象確認となりすましメール対策
  4. 2026.10.31 テックブログ osinaの情報漏洩:約23万人の対象確認とSNS連携の解除、分析ツールへ渡すデータの絞り方
  5. 2026.05.18 テックブログ マネーフォワードで連携できない・失敗するときの対処法|エラー別の原因と再連携のやり方

RELATED POSTS 関連記事

目次