PHPのforeachとは、配列やオブジェクトの要素を先頭から1つずつ取り出して、同じ処理を繰り返す構文です。foreach ($配列 as $値) で値だけ、foreach ($配列 as $キー => $値) でキーと値の両方を受け取れます。添字の範囲を自分で管理しなくてよいので、PHPで配列を回すときの第一候補になります。

この記事では、PHP 8.4.21(CLI)ですべてのコードを実行し、出力をそのまま載せています。やりたいこと別のコピペ一覧に加えて、参照渡し(&$value)のあとに unset() を書かないと最後の要素が書き換わる現象の再現、ループ中に配列を追加・削除したときの動き、null や文字列を渡したときのWarningと対処、for・while・array_map との使い分けまで、1ページで確認できます。

PHP foreachの早見表

コピーしてそのまま使える形にまとめました。$list は添字配列、$map は連想配列、$rows は「連想配列の配列」を想定しています。

やりたいこと 書き方 補足
値だけ取り出す foreach ($list as $v) { ... } 最も基本の形
キーと値を取り出す foreach ($map as $k => $v) { ... } 連想配列はこの形
キーだけ取り出す foreach (array_keys($map) as $k) { ... } 値を使わないとき
添字配列で何番目かを取る foreach ($list as $i => $v) { ... } $i は0から
連想配列で何番目かを取る foreach (array_keys($map) as $i => $k) { ... } 値は $map[$k]
多次元配列を回す foreach ($rows as $row) { foreach ($row['items'] as $item) { ... } } foreachを入れ子に
分割代入(キー指定) foreach ($rows as ['id' => $id, 'name' => $name]) { ... } PHP 7.1以降
分割代入(位置) foreach ($pairs as [$x, $y]) { ... } list($x, $y) でも同じ
途中で抜ける break; 入れ子を2段抜けるなら break 2;
その回だけ飛ばす continue; 次の要素へ進む
最初の要素か判定 $k === array_key_first($map) PHP 7.3以降
最後の要素か判定 $k === array_key_last($map) PHP 7.3以降
要素を書き換える foreach ($list as &$v) { ... } unset($v); unset までが1セット
参照を使わず書き換える foreach ($list as $i => $v) { $list[$i] = ...; } 読みやすく、後始末も不要
オブジェクトのプロパティ foreach ($obj as $prop => $v) { ... } 外からはpublicだけ
ジェネレータ foreach (gen() as $k => $v) { ... } 大きなデータを1件ずつ
nullかもしれない値 foreach ($maybe ?? [] as $v) { ... } Warningを出さずに0回で通過
HTMLの中で書く <?php foreach ($list as $v): ?> ... <?php endforeach; ?> テンプレート向けの別の書き方

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「PHP foreachの使い方|連想配列のキー取得と参照渡しの注意点」

foreachの基本の書き方(値だけ・キーと値)

値だけを取り出す

as の右に変数を1つ書くと、要素の値が順番に入ります。

<?php
$fruits = ['りんご', 'みかん', 'ぶどう'];

foreach ($fruits as $fruit) {
    echo $fruit, PHP_EOL;
}

実行結果(PHP 8.4.21):

りんご
みかん
ぶどう

連想配列のキーと値を取り出す

$キー => $値 の形にすると、キーも同時に受け取れます。変数名は自由なので、中身がわかる名前を付けると読みやすくなります。

<?php
$stock = ['pen' => 120, 'note' => 45, 'eraser' => 0];

foreach ($stock as $name => $count) {
    echo "{$name}: {$count}個", PHP_EOL;
}

実行結果:

pen: 120個
note: 45個
eraser: 0個

PHPの配列は追加した順番を保つため、連想配列でも書いた順に取り出されます。

インデックス(何番目か)を取得する

添字配列なら、キーがそのまま0始まりの番号です。

<?php
$fruits = ['りんご', 'みかん', 'ぶどう'];

foreach ($fruits as $i => $fruit) {
    echo ($i + 1) . "番目: {$fruit}", PHP_EOL;
}

実行結果:

1番目: りんご
2番目: みかん
3番目: ぶどう

連想配列のキーは文字列なので、番号が欲しいときは次のどちらかを使います。

<?php
$stock = ['pen' => 120, 'note' => 45, 'eraser' => 0];

// 方法1: カウンタ変数を自分で進める
$i = 0;
foreach ($stock as $name => $count) {
    echo "{$i}: {$name}", PHP_EOL;
    $i++;
}

// 方法2: array_keys() を回して、番号とキーの両方を受け取る
foreach (array_keys($stock) as $i => $name) {
    echo "{$i}: {$name} = {$stock[$name]}", PHP_EOL;
}

実行結果:

0: pen
1: note
2: eraser
0: pen = 120
1: note = 45
2: eraser = 0
方法 向いている場面
カウンタ変数 continue で飛ばした回を数えたくないなど、数え方を自分で決めたいとき
array_keys() を回す 番号・キー・値の3つをすっきり受け取りたいとき
array_values() を回す キーが不要で、0からの連番に振り直したいとき

HTMLの中で書くとき(endforeach)

テンプレートでは、波かっこの代わりに : と endforeach; を使う書き方が読みやすくなります。動きは同じです。

<ul>
<?php foreach ($fruits as $fruit): ?>
<li><?= htmlspecialchars($fruit) ?></li>
<?php endforeach; ?>
</ul>

実行結果(<ul> の内側):

<li>りんご</li>
<li>みかん</li>
<li>ぶどう</li>

多次元配列と分割代入

多次元配列はforeachを入れ子にする

データベースの検索結果やJSONを配列にしたものは、「連想配列の配列」になっていることがほとんどです。外側で1行分を取り出し、内側でその中の配列を回します。

<?php
$orders = [
    ['id' => 101, 'member' => '佐藤', 'items' => ['ペン', 'ノート']],
    ['id' => 102, 'member' => '鈴木', 'items' => ['消しゴム']],
];

foreach ($orders as $order) {
    echo "注文{$order['id']}({$order['member']}): ";
    foreach ($order['items'] as $item) {
        echo $item, ' ';
    }
    echo PHP_EOL;
}

実行結果:

注文101(佐藤): ペン ノート 
注文102(鈴木): 消しゴム 

分割代入で必要なキーだけ変数にする

as の右側に ['キー' => $変数] と書くと、取り出しと変数への代入を一度に済ませられます。$order['id'] のような添字アクセスが減り、ループ本体が短くなります。

<?php
$orders = [
    ['id' => 101, 'member' => '佐藤', 'items' => ['ペン', 'ノート']],
    ['id' => 102, 'member' => '鈴木', 'items' => ['消しゴム']],
];

// キーを指定する分割代入
foreach ($orders as ['id' => $id, 'member' => $member]) {
    echo "{$id}: {$member}", PHP_EOL;
}

// 位置で受け取る分割代入
$points = [[1, 2], [3, 4]];
foreach ($points as [$x, $y]) {
    echo "x={$x}, y={$y}", PHP_EOL;
}

// 外側のキー + 入れ子の分割代入
foreach ($orders as $i => ['id' => $id, 'items' => [$first]]) {
    echo "{$i}: {$id} の最初の商品は {$first}", PHP_EOL;
}

実行結果:

101: 佐藤
102: 鈴木
x=1, y=2
x=3, y=4
0: 101 の最初の商品は ペン
1: 102 の最初の商品は 消しゴム

分割代入で存在しないキーを指定すると、Warningが出て変数には null が入ります。

foreach ($orders as ['id' => $id, 'note' => $note]) {
    var_dump($note);
    break;
}
Warning: Undefined array key "note" in /app/02_nested.php on line 27
NULL

(ファイルのパスは短く書き換えています。以降のWarningも同じです)

キーがあるかどうかわからない行が混ざるときは、分割代入を使わずに $order['note'] ?? '' のように ?? で既定値を決めると、Warningなしで扱えます。

breakとcontinueでループを制御する

continue は「その回だけ飛ばして次へ」、break は「ループそのものを終える」という違いです。

<?php
$tasks = ['設計', '', '実装', 'STOP', 'テスト'];

foreach ($tasks as $task) {
    if ($task === '') {
        continue;   // 空はスキップ
    }
    if ($task === 'STOP') {
        break;      // ここで終了
    }
    echo $task, PHP_EOL;
}

実行結果:

設計
実装

入れ子のループをまとめて抜けたいときは、break に数字を付けます。

<?php
$grid = [[1, 2, 3], [4, 5, 6], [7, 8, 9]];

foreach ($grid as $row) {
    foreach ($row as $n) {
        if ($n === 5) {
            break 2;   // 外側のループも抜ける
        }
        echo $n, ' ';
    }
}

実行結果:

1 2 3 4 
書き方 動き
continue; いま回っているループの次の要素へ進む
continue 2; 1つ外側のループの次の要素へ進む
break; いま回っているループを終える
break 2; 1つ外側のループまで終える

なお、PHP 8.4には「条件に合う最初の要素を探す」array_find() や、「1つでも当てはまるか」を返す array_any() が入りました。「見つけたらbreak」だけのループは、これらの関数1行に置き換えられます(後半の使い分け表で実行例を載せています)。

最初と最後の要素を判定する

区切り文字を最後だけ付けない、先頭行だけ見出しにする、といった処理では「いま最初か・最後か」を知りたくなります。array_key_first() と array_key_last() でキーを先に取っておき、ループの中で比べるのが、添字配列でも連想配列でも使える書き方です。

<?php
$stock = ['pen' => 120, 'note' => 45, 'eraser' => 0];

$first = array_key_first($stock);
$last  = array_key_last($stock);

foreach ($stock as $name => $count) {
    $mark = match (true) {
        $name === $first => '[最初]',
        $name === $last  => '[最後]',
        default          => '      ',
    };
    echo "{$mark} {$name}", PHP_EOL;
}

実行結果:

[最初] pen
       note
[最後] eraser
判定方法 連想配列 歯抜けの添字 メモ
$k === array_key_first($a) / array_key_last($a) 使える 使える PHP 7.3以降。おすすめ
$i === 0 / $i === count($a) - 1 使えない ずれる キーが0からの連番のときだけ
カウンタ変数と count($a) を比べる 使える 使える PHP 7.2以前でも書ける

空の配列に対して array_key_first() を呼ぶと null が返り、foreachの本体は1回も実行されません。そのため空配列でもそのまま動きます。

NULL
ループ本体は0回

「カンマ区切りにしたいだけ」なら、ループを書かずに implode() で済みます。

echo implode(', ', array_keys($stock)), PHP_EOL;   // pen, note, eraser

参照渡しで要素を書き換える(unsetまでが1セット)

参照渡しの書き方

foreachで受け取る $value は、ふつうは要素のコピーです。$value に代入しても元の配列は変わりません。&$value と書くと元の要素への参照になり、代入がそのまま配列に反映されます。

<?php
$prices = [100, 200, 300];

foreach ($prices as &$price) {
    $price = $price * 2;
}
unset($price);   // 参照を切る

print_r($prices);

実行結果:

Array
(
    [0] => 200
    [1] => 400
    [2] => 600
)

unsetを書かないと最後の要素が書き換わる(再現)

参照渡しのループを抜けたあとも、$price は配列の最後の要素とつながったまま残ります。この状態で同じ変数名をもう一度使うと、思わぬ書き換えが起きます。次のコードは、2つ目のループで「表示するだけ」のつもりです。

<?php
$prices = [100, 200, 300];

foreach ($prices as &$price) {
    $price = $price * 2;
}
// unset($price); を書いていない

print_r($prices);

foreach ($prices as $price) {
    // 表示するだけのつもり
}

print_r($prices);

実行結果(PHP 8.4.21):

Array
(
    [0] => 200
    [1] => 400
    [2] => 600
)
Array
(
    [0] => 200
    [1] => 400
    [2] => 400
)

2つ目のループでは何も代入していないのに、最後の要素が 600 から 400 に変わりました。エラーもWarningも出ないため、気づきにくい現象です。

なぜそうなるのか(1周ずつ追う)

2つ目のループの各周で配列の中身を出力すると、仕組みが見えてきます。

<?php
$prices = [100, 200, 300];
foreach ($prices as &$price) {
    $price = $price * 2;
}
foreach ($prices as $i => $price) {
    echo "{$i}周目: ", json_encode($prices), PHP_EOL;
}

実行結果:

0周目: [200,400,200]
1周目: [200,400,400]
2周目: [200,400,400]
周 foreachがすること $price(=$prices[2])の値
0周目 $prices[0] の200を $price に代入 200
1周目 $prices[1] の400を $price に代入 400
2周目 $prices[2](いま400)を $price に代入 400のまま

$price は $prices[2] の別名になっているので、「$price に代入する」=「$prices[2] に代入する」になります。1周目で最後の要素が400に上書きされ、2周目は自分自身を代入するだけなので、最後の要素は1つ手前と同じ値になります。

flowchart LR
    subgraph A["1つ目のループ終了後"]
        P["変数 price"] -. 参照 .-> E2["prices 2番 = 600"]
    end
    subgraph B["2つ目のループ 1周目"]
        E1["prices 1番 = 400"] -- 代入 --> P2["変数 price"]
        P2 -. 参照 .-> E22["prices 2番 = 400 に変化"]
    end
    A --> B

参照が残っているかどうかは var_dump() で確かめられます。値の前に & が付いている要素が、まだ参照でつながっている要素です。

<?php
$a = [1, 2, 3];
foreach ($a as &$v) {}
var_dump($a);

$b = $a;      // 配列をコピーしたつもり
$b[2] = 99;
echo json_encode($a), PHP_EOL;

実行結果:

array(3) {
  [0]=>
  int(1)
  [1]=>
  int(2)
  [2]=>
  &int(3)
}
[1,2,99]

参照が残った要素は、配列を別の変数に代入しても共有されたままになります。上の例では $b を書き換えたのに $a の最後の要素も99になりました。同じ変数名を使い回していなくても影響が出るという点で、unset() を書いておく意味は大きいです。

直し方は3通り

<?php
// 直し方1: ループの直後に unset() を書く
$prices = [100, 200, 300];
foreach ($prices as &$price) {
    $price = $price * 2;
}
unset($price);

// 直し方2: 参照を使わず、キーで代入する
$prices = [100, 200, 300];
foreach ($prices as $i => $price) {
    $prices[$i] = $price * 2;
}
echo json_encode($prices), PHP_EOL;

// 直し方3: array_map() で新しい配列を作る
$prices = [100, 200, 300];
$prices = array_map(fn (int $p): int => $p * 2, $prices);
echo json_encode($prices), PHP_EOL;

実行結果:

[200,400,600]
[200,400,600]
直し方 よいところ 向いている場面
&$v +直後に unset($v) 入れ子の配列の奥も書き換えやすい 多次元配列の一部を更新するとき
$a[$k] = ... でキー代入 参照が登場しないので後始末が不要 迷ったらこれ
array_map() 元の配列を残したまま新しい配列を作れる 全要素を同じルールで変換するとき

PHPマニュアルのforeachのページにも、最後の要素への参照がループ後も残ること、unset() で切るのがおすすめであることが書かれています。& を書いたら、閉じかっこの次の行に unset() を書く、と手の動きで覚えておくと安心です。

ループ中に配列を変更したときの挙動

「回している最中の配列に要素を足したり消したりしたらどうなるのか」は、値渡しと参照渡しで結果が変わります。PHP 8.4.21で4パターンを実行しました。

<?php
echo "--- 値渡し: ループ中に追加\n";
$a = [1, 2, 3];
foreach ($a as $v) {
    if ($v === 1) {
        $a[] = 4;       // ループ中に追加
    }
    echo $v, ' ';
}
echo PHP_EOL, json_encode($a), PHP_EOL;

echo "--- 値渡し: ループ中に削除\n";
$a = [1, 2, 3];
foreach ($a as $k => $v) {
    if ($k === 0) {
        unset($a[1]);   // まだ回っていない要素を削除
    }
    echo $v, ' ';
}
echo PHP_EOL, json_encode($a), PHP_EOL;

echo "--- 参照渡し: ループ中に追加\n";
$a = [1, 2, 3];
foreach ($a as &$v) {
    if ($v === 1) {
        $a[] = 4;
    }
    echo $v, ' ';
}
unset($v);
echo PHP_EOL, json_encode($a), PHP_EOL;

echo "--- 参照渡し: ループ中に削除\n";
$a = [1, 2, 3];
foreach ($a as $k => &$v) {
    if ($k === 0) {
        unset($a[1]);
    }
    echo $v, ' ';
}
unset($v);
echo PHP_EOL, json_encode($a), PHP_EOL;

実行結果:

--- 値渡し: ループ中に追加
1 2 3 
[1,2,3,4]
--- 値渡し: ループ中に削除
1 2 3 
{"0":1,"2":3}
--- 参照渡し: ループ中に追加
1 2 3 4 
[1,2,3,4]
--- 参照渡し: ループ中に削除
1 3 
{"0":1,"2":3}
渡し方 ループ中に追加 ループ中に削除 理由
値渡し(as $v) 追加分は回らない 削除した要素も回る ループ開始時点の配列のコピーを回すため
参照渡し(as &$v) 追加分も回る 削除した要素は回らない 元の配列そのものを回すため

値渡しのforeachは、開始時点の配列を写し取って回します。そのため、ループの中で元の配列を変更しても、回る回数や順番は変わりません(PHP 7.0以降の動き)。「コピー」といっても、実際にメモリが複製されるのは配列を書き換えた瞬間だけなので、読むだけのループで配列が大きくても心配はいりません。

この性質のおかげで、条件に合う要素をループの中で unset() する書き方は、値渡しなら順番がずれずに動きます。

<?php
$stock = ['pen' => 120, 'note' => 45, 'eraser' => 0, 'clip' => 0];

foreach ($stock as $name => $count) {
    if ($count === 0) {
        unset($stock[$name]);
    }
}
echo json_encode($stock), PHP_EOL;

実行結果:

{"pen":120,"note":45}

同じ処理は array_filter($stock, fn ($c) => $c > 0) でも書けます。添字配列で unset() すると添字が歯抜けになる(上の実行結果の {"0":1,"2":3})ので、連番に戻したいときは array_values() を通します。

あわせて知っておくと便利な性質を2つ挙げます。

  • foreachは配列の内部ポインタを動かしません。ループ後に current($a) を呼ぶと、先頭の値 int(1) が返りました。
  • ループ変数はループを抜けたあとも残ります。foreach ([10, 20, 30] as $k => $v) {} のあと、$k は int(2)、$v は int(30) でした。次の処理で同じ変数名を使うときは、値が残っていることを意識しておきましょう。

nullや文字列を渡したときのWarningと対処

Warningを再現する

foreachに渡せるのは配列とオブジェクトです。それ以外を渡すとWarningが出て、ループ本体は実行されずに次の行へ進みます。

<?php
$list = null;
foreach ($list as $v) { echo $v; }
echo "null のあとも処理は続く\n";

foreach ('abc' as $v) { echo $v; }
foreach (123 as $v) { echo $v; }
foreach ($undefined as $v) { echo $v; }
echo "end\n";

実行結果(PHP 8.4.21):

Warning: foreach() argument must be of type array|object, null given in /app/09_warning.php on line 3
null のあとも処理は続く
Warning: foreach() argument must be of type array|object, string given in /app/09_warning.php on line 6
Warning: foreach() argument must be of type array|object, int given in /app/09_warning.php on line 7
Warning: Undefined variable $undefined in /app/09_warning.php on line 8
Warning: foreach() argument must be of type array|object, null given in /app/09_warning.php on line 8
end
メッセージの末尾 よくある原因
null given 検索結果が0件で null が返った、配列のキーが存在しなかった、変数名の打ち間違い
string given JSON文字列を json_decode() する前に回した、1件だけのときに文字列で届いた
int given / bool given 件数や成否を返す関数の戻り値をそのまま回した(false を返す関数など)

PHP 7系では同じ状況で Invalid argument supplied for foreach() というメッセージでした。PHP 8.0以降は上の文面です。

対処の書き方5つ

<?php
declare(strict_types=1);

function findTags(int $articleId): ?array
{
    return $articleId === 1 ? ['php', 'loop'] : null;
}

// 対処1: null 合体演算子で空配列にする
foreach (findTags(2) ?? [] as $tag) {
    echo $tag, PHP_EOL;
}
echo "対処1: 0回で通過\n";

// 対処2: is_iterable() で確かめる
$value = 'abc';
if (is_iterable($value)) {
    foreach ($value as $v) { echo $v; }
} else {
    echo "対処2: 配列でもTraversableでもない\n";
}

// 対処3: 文字列を1文字ずつ回したいとき
foreach (mb_str_split('こんにちは') as $ch) {
    echo $ch, ' ';
}
echo PHP_EOL;

// 対処4: 1件でも複数でも回せるようにそろえる
$input = 'php';
foreach ((array) $input as $tag) {
    echo "tag={$tag}", PHP_EOL;
}

// 対処5: 型宣言で入口をそろえる
function printAll(iterable $items): void
{
    foreach ($items as $item) { echo $item, ' '; }
    echo PHP_EOL;
}
printAll(['a', 'b']);
try {
    printAll(null);
} catch (TypeError $e) {
    echo get_class($e), ': ', $e->getMessage(), PHP_EOL;
}

実行結果:

対処1: 0回で通過
対処2: 配列でもTraversableでもない
こ ん に ち は 
tag=php
a b 
TypeError: printAll(): Argument #1 ($items) must be of type Traversable|array, null given, called in /app/10_warning_fix.php on line 43
対処 書き方 向いている場面
空配列にそろえる foreach ($x ?? [] as $v) null のときは何もしなくてよいとき
種類を確かめる if (is_iterable($x)) 配列以外が来たら別の処理をしたいとき
文字列を配列にする mb_str_split($s) 1文字ずつ回したいとき
配列に変換する (array) $x 1件のとき文字列、複数のとき配列で届く入力
型宣言で受ける function f(iterable $items) 関数の入口で種類を決めておきたいとき

いちばん効くのは、配列を返す関数が「0件のときも空配列 [] を返す」ようにそろえることです。戻り値の型を ?array ではなく array にしておけば、呼び出し側は何も気にせずforeachに渡せます。なお (array) null は空配列になりますが、(array) をオブジェクトに使うとプロパティの配列に変換されるので、スカラー値と配列だけに使うのがわかりやすいです。

オブジェクトをforeachで回す

プロパティを順に取り出す

オブジェクトをforeachに渡すと、その場所から見えるプロパティが順に取り出されます。クラスの外からはpublicだけ、クラスの中($this)からはprotectedとprivateも含まれます。

<?php
class Member
{
    public string $name = '佐藤';
    public int $age = 30;
    protected string $email = 'sato@example.com';
    private string $token = 'secret';

    public function dump(): void
    {
        foreach ($this as $k => $v) { echo "  {$k}={$v}", PHP_EOL; }
    }
}

$m = new Member();
echo "外から:\n";
foreach ($m as $prop => $value) {
    echo "  {$prop}={$value}", PHP_EOL;
}
echo "中から:\n";
$m->dump();

実行結果:

外から:
  name=佐藤
  age=30
中から:
  name=佐藤
  age=30
  email=sato@example.com
  token=secret

json_decode() の戻り値(stdClass)も同じように回せます。列挙型(enum)は Status::cases() で配列にしてから回します。

<?php
$obj = json_decode('{"id":1,"title":"foreach入門"}');
foreach ($obj as $k => $v) { echo "{$k}={$v}", PHP_EOL; }

enum Status: string { case Draft = 'draft'; case Public = 'public'; }
foreach (Status::cases() as $case) { echo $case->name, '=', $case->value, PHP_EOL; }
id=1
title=foreach入門
Draft=draft
Public=public

自作クラスを「回せる」ようにする(IteratorAggregate)

IteratorAggregate を実装して getIterator() を用意すると、クラスの中の配列をforeachで回せるようになります。コレクション用のクラスを作るときの定番の形です。

<?php
class TaskList implements IteratorAggregate, Countable
{
    private array $tasks = [];

    public function add(string $t): static
    {
        $this->tasks[] = $t;
        return $this;
    }

    public function getIterator(): Iterator
    {
        return new ArrayIterator($this->tasks);
    }

    public function count(): int
    {
        return count($this->tasks);
    }
}

$list = (new TaskList())->add('設計')->add('実装');
foreach ($list as $i => $task) {
    echo "{$i}: {$task}", PHP_EOL;
}

実行結果:

0: 設計
1: 実装

ジェネレータをforeachで回す

ジェネレータは、yield で値を1件ずつ渡す関数です。全件を配列に入れてから回すのと違い、必要になった分だけ作るので、大きなファイルや大量の行を扱うときにメモリを節約できます。

<?php
function readLines(string $path): Generator
{
    $fp = fopen($path, 'r');
    try {
        while (($line = fgets($fp)) !== false) {
            yield rtrim($line, "\n");
        }
    } finally {
        fclose($fp);
    }
}

file_put_contents('/tmp/lines.txt', "1行目\n2行目\n3行目\n");

foreach (readLines('/tmp/lines.txt') as $no => $line) {
    echo ($no + 1), ': ', $line, PHP_EOL;
}

// キーつきで yield する
function squares(int $max): Generator
{
    for ($i = 1; $i <= $max; $i++) {
        yield "n{$i}" => $i * $i;
    }
}
foreach (squares(3) as $k => $v) {
    echo "{$k} => {$v}", PHP_EOL;
}

実行結果:

1: 1行目
2: 2行目
3: 3行目
n1 => 1
n2 => 4
n3 => 9

100万件を合計する処理で、ピーク時のメモリ増加を比べました。

<?php
function numbers(int $max): Generator
{
    for ($i = 0; $i < $max; $i++) { yield $i; }
}

$base = memory_get_usage();
$sum = 0;
foreach (numbers(1_000_000) as $n) { $sum += $n; }
printf("generator: sum=%d, 増加 %.1f KB\n", $sum, (memory_get_peak_usage() - $base) / 1024);

$base = memory_get_peak_usage();
$sum = 0;
foreach (range(0, 999_999) as $n) { $sum += $n; }
printf("range配列: sum=%d, 増加 %.1f MB\n", $sum, (memory_get_peak_usage() - $base) / 1024 / 1024);

実行結果(PHP 8.4.21。数値は環境で変わります):

generator: sum=499999500000, 増加 34.8 KB
range配列: sum=499999500000, 増加 16.0 MB

ジェネレータは一度しか回せない点が配列と違います。同じジェネレータを2回目のforeachに渡すと例外になります。

1 4 
Exception: Cannot traverse an already closed generator

もう一度回したいときは、関数を呼び直して新しいジェネレータを作るか、iterator_to_array() で配列にしておきます。

for・while・array_mapとの使い分け

使い分けの判断表

構文・関数 向いている場面 書き方の例
foreach 配列・オブジェクトの全要素を順に処理する(第一候補) foreach ($a as $k => $v)
for 回数が決まっている、1つ飛ばしや逆順など添字を自分で動かしたい for ($i = 0; $i < 10; $i++)
while 終わる条件が「データ」で決まる(読み切るまで、キューが空になるまで) while (($row = fgets($fp)) !== false)
array_map() 全要素を同じルールで変換し、新しい配列がほしい array_map(fn ($v) => $v * 2, $a)
array_filter() 条件に合う要素だけ残したい array_filter($a, fn ($v) => $v > 0)
array_sum() / array_reduce() 合計や集計を1つの値にまとめたい array_sum($a)
array_find() / array_any() / array_all() 探す・1つでもあるか・すべて満たすか(PHP 8.4以降) array_find($a, fn ($v) => $v < 100)

選び方の目安は、「ループの中でいろいろな処理をする・途中で抜ける」ならforeach、「配列から別の配列や1つの値を作るだけ」なら配列関数です。配列関数は「何をしたいか」が関数名に表れるので、読む人に意図が伝わりやすくなります。配列関数の全体像は、既存記事「PHP配列関数一覧|8.4/8.5新関数まで目的別に実行例つき」にまとめています。

同じ配列で書き比べる

<?php
$names = ['sato', 'suzuki', 'tanaka'];

// for: count() は初期化で1回だけ呼ぶ
for ($i = 0, $n = count($names); $i < $n; $i++) {
    echo $names[$i], ' ';
}
echo PHP_EOL;

// for は歯抜けのキーが苦手
$sparse = [0 => 'a', 2 => 'c'];
for ($i = 0; $i < count($sparse); $i++) {
    echo $sparse[$i] ?? '(なし)', ' ';
}
echo PHP_EOL;
foreach ($sparse as $k => $v) {
    echo "{$k}={$v} ";
}
echo PHP_EOL;

// while: 取り出すものがなくなるまで
$queue = ['a', 'b', 'c'];
while (($item = array_shift($queue)) !== null) {
    echo $item, ' ';
}
echo PHP_EOL;

// array_map / array_sum
echo json_encode(array_map(strtoupper(...), $names)), PHP_EOL;

$stock = ['pen' => 120, 'note' => 45, 'eraser' => 0];
echo array_sum($stock), PHP_EOL;

// array_map でキーも使いたいとき
echo json_encode(array_map(fn ($k, $v) => "{$k}:{$v}", array_keys($stock), $stock)), PHP_EOL;

// PHP 8.4 の新しい関数
var_dump(array_find($stock, fn ($c) => $c < 100));
var_dump(array_any($stock, fn ($c) => $c === 0));
var_dump(array_all($stock, fn ($c) => $c >= 0));

実行結果(PHP 8.4.21):

sato suzuki tanaka 
a (なし) 
0=a 2=c 
a b c 
["SATO","SUZUKI","TANAKA"]
165
["pen:120","note:45","eraser:0"]
int(45)
bool(true)
bool(true)

歯抜けの配列 [0 => 'a', 2 => 'c'] をforで回すと、count() が2なので添字0と1を見にいき、存在しない1番を読んで、実在する2番には届きません(上の例は ?? で「(なし)」と表示しています)。foreachは実在する要素だけを順に取り出すので、unset() や array_filter() を通したあとの配列でもそのまま使えます。

速度の目安

100万要素の合計を3通りで計測しました。

foreach 10.3ms / for 15.8ms / array_sum 2.0ms

PHP 8.4.21での1回の計測値で、環境によって変わります。この計測ではforeachがforより速く、専用の関数(array_sum)がさらに速いという結果でした。ふだんは速度よりも読みやすさで選び、専用の関数があればそれを使う、という方針で十分です。

つまずきやすい点と直し方

起きること 原因 直し方
最後の要素が1つ手前と同じ値になる &$v のあとに unset($v) がなく、同じ変数名を再利用した ループ直後に unset($v);。またはキー代入に変える
$v に代入しても配列が変わらない 値渡しの $v はコピー $a[$k] = ... と書く。または &$v
foreach() argument must be of type array|object, null given 渡した値が null ?? [] を付ける。関数は空配列を返すようにする
Undefined array key "..." 分割代入や $row['key'] で存在しないキーを指定 $row['key'] ?? 既定値 を使う
forで回すと Undefined array key が出る 配列の添字が歯抜け foreachで回す。または array_values() で振り直す
Cannot traverse an already closed generator ジェネレータを2回回した 関数を呼び直す。または iterator_to_array()
ループ中に足した要素が処理されない 値渡しは開始時点のコピーを回す 処理待ちの配列を while と array_shift() で回す
ループ後に $k や $v に値が残っている ループ変数はループ後も有効 次の処理では別の変数名を使う

foreachを書くときのチェックリスト

  • [ ] 値だけでよいか、キーも必要かを決めて as $v / as $k => $v を選んだ
  • [ ] &$v を使ったループの直後に unset($v); を書いた
  • [ ] 渡す値が null になり得るなら ?? [] を付けた(または空配列を返すようにした)
  • [ ] 「変換するだけ」「絞り込むだけ」「合計するだけ」なら配列関数を検討した
  • [ ] 最初・最後の判定は array_key_first() / array_key_last() を使った
  • [ ] 大きなデータは、全件を配列にせずジェネレータで1件ずつ回せないか考えた
  • [ ] HTMLに出力する値は htmlspecialchars() を通した

よくある質問

Q. foreachでキー(連想配列のキー名)だけを取得するには? A. foreach ($map as $key => $value) と書いて $key だけを使うか、foreach (array_keys($map) as $key) と書きます。値を使わないことが伝わりやすいのは後者です。キーの一覧が配列としてほしいだけなら、ループを書かずに array_keys($map) で取得できます。

Q. 参照渡しのあとの unset($value) は、配列の要素を消してしまいませんか? A. 消しません。unset($value) が外すのは「変数 $value と配列の要素とのつながり」だけで、配列の中身はそのまま残ります。この記事の実行結果でも、unset($price) のあとに [200, 400, 600] がそのまま出力されています。

Q. foreachで回している途中に、いま何番目かを知るには? A. 添字配列なら foreach ($list as $i => $v) の $i が0始まりの番号です。連想配列ならカウンタ変数を自分で $i++ するか、foreach (array_keys($map) as $i => $key) と書いて番号とキーを同時に受け取ります。

Q. foreachとforはどちらを使えばよいですか? A. 配列の全要素を順に処理するならforeachが向いています。添字の範囲を管理する必要がなく、連想配列や歯抜けの配列でも同じ書き方で動きます。「10回繰り返す」「1つ飛ばしで進む」「後ろから前へ進む」のように添字を自分で動かしたいときはforが向いています。

まとめ

  • 基本の形は2つ。値だけなら foreach ($a as $v)、連想配列のキーも使うなら foreach ($a as $k => $v)
  • 分割代入 ['id' => $id] を使うと、多次元配列のループ本体が短く読みやすくなる
  • 最初と最後の判定は array_key_first() / array_key_last() が連想配列にも歯抜けの配列にも使える
  • 参照渡し &$v は unset($v) までが1セット。書かないと、次のループで最後の要素が書き換わる
  • 値渡しのforeachは開始時点のコピーを回すので、ループ中に追加した要素は回らず、unset() しても順番はずれない
  • null を渡すとWarning。?? [] を付けるか、関数が空配列を返すようにそろえる
  • 変換・絞り込み・集計だけなら配列関数(array_map・array_filter・array_sum、PHP 8.4の array_find など)が意図を伝えやすい

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「PHP foreachの使い方|連想配列のキー取得と参照渡しの注意点」

株式会社RJCでは、PHPのforeachのような基本の構文を仲間と学び合っています。こうした知識を共有しながら、日々技術を磨いています。

株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。

WE ARE HIRING

RJCで一緒に開発しながら、
成長を楽しみませんか?

RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!

  • 127日年間休日
  • 12時間平均残業時間
  • 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!

ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。

ABOUT RJC RJCがどんな会社か知る 考え方、研修、働き方、福利厚生、社員の前職まで。RJCのことが丸わかり! RJC丸わかりページへ JOB DESCRIPTION 仕事内容・待遇を見てみる 仕事内容、給与・待遇、選考の流れ。経験者も未経験も!応募前に知りたいこと、まとめました! 募集要項を見る ENTRY エントリーする エントリーは1〜2分・履歴書不要です。まずは話を聞いてみたい、という方でも歓迎です! エントリーフォームへ

RJCで一緒に開発しながら、 成長を楽しみませんか?