メシのタネ

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


Laravel Collection::each()──愛と裏切りの副作用【returnが効かない理由と安全な書き方】


  1. Laravel
  2. Laravel Collection::each()──愛と裏切りの副作用【returnが効かない理由と安全な書き方】

Laravel の Collection::each() は便利な反面、returnbreak が効かず副作用を招きやすい──本記事では Pest で罠を再現しながら、安全な使い方・代替メソッド・例外処理テクニックを徹底解説します。

今回のPestのテストはこちらにupしてますので、確認しながら学習がおすすめ → https://github.com/wasipo/syugyou/blob/main/tests/Unit/laravel-collection-each-side-effects.php

本記事は「Laravel Collectionにおけるeach()の挙動と注意点」に特化した技術記事です。
mapやfilterとの比較や入門解説ではなく、「eachでハマりがちな罠」「安全な使い方」「Pestでの挙動確認」など、
挙動ベースの深掘り検証にフォーカスしています!!

mapなどの解説を見ながら学びたい方は👉️ map vs each完全ガイド
mapの応用をコード付きで学びたい方は👉️ Collection実践キャンプ
当たり前ですが、どっちも無料です。(キャンプってお金かかりそうだし)

each でループを途中で止めたい?実は止まりません。

Laravel Collection の each() メソッド、foreach の代わりに使いたくなるあの手軽さ。でも待って。
実は、便利そうに見える each() は return や break が効かず、副作用を招きがちな設計です。

本記事では Pest を使い、each() の落とし穴を検証しながら、安全な使い方と代替手段を紹介します。

注:本記事のタイトルでCollection::each()とありますが実際に静的呼び出しすることはできません。

each は return が効かない(実装と検証)

collect([1, 2, 3])->each(function ($item) {
    if ($item === 2) {
        return; // 2だけスキップ、ループは止まらない
    }
    echo $item;
});

上記は 13 を出力します。2 のとき return しても ループは止まりません

Pest テストコード(目的:ループが中断されないことの確認)

describe('Collection::each の挙動検証', function () {
    it('does not break each with return', function () {
        $output = [];

        collect([1, 2, 3])->each(function ($item) use (&$output) {
            if ($item === 2) {
                return;
            }
            $output[] = $item;
        });

        // 想定出力: [1, 3]。2 はスキップされるが、ループは止まらない。
        expect($output)->toBe([1, 3]);
    });
});

break や continue が効かない(Breakできるのか?)

foreach ($items as $item) {
    if ($item->shouldStop()) {
        break;
    }
}

この「途中で止まる」処理、each() では 物理的に不可能 です。

Pest テストコード(目的:foreach なら break 可能なことの確認)

it('can break with foreach but not with each', function () {
    $items = collect([1, 2, 3, 4]);
    $output = [];

    foreach ($items as $item) {
        if ($item === 3) {
            break;
        }
        $output[] = $item;
    }

    // 想定出力: [1, 2]
    expect($output)->toBe([1, 2]);
});

副作用の処理には向かない

collect($users)->each(fn($u) => $u->save());

見た目はキレイ。でも save() が失敗したら、どのデータが失敗したか分からず、ログも残らず、ループも止まる 危険があります。

Pest テストコード(目的:例外が飛ぶと後続が実行されないことの確認)

it('throws and halts on save failure with each', function () {
    $users = collect([
        Mockery::mock(User::class)->shouldReceive('save')->andThrow(new Exception('save failed'))->getMock(),
        Mockery::mock(User::class)->shouldNotReceive('save')->getMock(),
    ]);

    expect(function () use ($users) {
        $users->each(fn($u) => $u->save());
    })->toThrow(Exception::class);
});

try-catch で each を安全に使う

collect($users)->each(function ($user) {
    try {
        $user->save();
    } catch (\Throwable $e) {
        Log::error("保存失敗: {$user->id}");
    }
});

Pest テストコード(目的:例外を catch して処理継続することの確認)

it('logs error and continues when using try-catch inside each', function () {
    Log::shouldReceive('error')->once()->with('保存失敗: 1');

    $users = collect([
        Mockery::mock(User::class)->shouldReceive('save')->andThrow(new Exception)->getMock(),
        Mockery::mock(User::class)->shouldReceive('save')->once()->getMock(),
    ]);

    $users->each(function ($user, $index) {
        try {
            $user->save();
        } catch (\Throwable $e) {
            Log::error("保存失敗: " . ($index + 1));
        }
    });

    expect(true)->toBeTrue();
});

map を使えば副作用を回避できる

副作用を避けたいなら map() を使って新しいコレクションを返す形にしましょう。

$names = collect([
    ['name' => 'Alice'],
    ['name' => 'Bob'],
])->map(fn($user) => $user['name']);

// 結果: ['Alice', 'Bob']

このように、純粋関数的に値を返す用途には map() の方が安全かつ明示的

Laravelの他のループ処理とは?

メソッド説明
eachクロージャ実行、return/break 非対応
map新しいコレクション返す(副作用なし)
filter条件でフィルタリング
reject条件を除外
reduce値を一つにまとめる
tapメソッドチェーン内に副作用挟む(メモリに注意)

それぞれの使い時は?

  • each → 副作用が軽い、ログ出力、view 描画など(中断できない)
  • foreach → 中断・例外処理が必要な時(多くの処理に適してる)
  • map → 値を加工・変換する時(新コレクションが必要)
  • filter → 条件に合った要素だけ残す時
  • reduce → 合計や統計など、まとめ処理に
  • tap → メソッドチェーンの途中に副作用を差し込みたい時
  • mapeachを詳しくまなべる記事は 👉️ こちら

🎯 まとめ

  • each() は return / break が効かない「止まらない列車」
  • 副作用があるなら try-catch + ログは必須
  • 制御性・安全性が必要なら foreach / map / chunkById を検討
  • Pest で「each は止まらないこと」をテストして保証しよう
  • 副作用を避けたいなら map() でデータ変換に徹するのが吉
  • 本記事のテストはGithubに追加してるよ!

次のおすすめはコチラ

Laravel
実践 Laravel Collection パターン7選
Laravel
Laravel Collection入門: mapとeachの違い、ちゃんと説明できますか?
Laravel
Laravelの多対多リレーション実践運用大全(テストもあるよ!)

FAQ

each() はパフォーマンス的に遅い?

ほぼ同じ要素数なら foreach と大差ありません。ただし return/break が効かず “最後まで回り切る” ため、早期脱出できる処理と比べるとムダが増えるケースがあります。

途中で save() が失敗したらロールバックできる?

each() 単体ではトランザクション制御ができません。

each() の代替として map() を使うべきタイミングは?

純粋関数的に値を変換したいだけ

戻り値のコレクションが必要

副作用を避けたい/テストしやすさを重視

デバッグ時に each() の中で何が起こっているか確認したい

each処理内でdump()を使うのが一番お手軽。


コメントを残す

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

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