PHPのforeachでは、値変数の前に&を付けることで、配列の各要素を参照として扱えます。
$array = [1, 2, 3];
foreach ($array as &$value) {
$value *= 2;
}
unset($value);
print_r($array);
実行結果は次のようになります。
Array
(
[0] => 2
[1] => 4
[2] => 6
)
通常のforeachでは、ループ変数を書き換えても元の配列要素は変更されません。
しかし、foreach ($array as &$value)のように&を付けると、$valueは配列要素への参照になります。
そのため、ループ内で$valueを変更すると、元の配列の中身も直接変更されます。
ただし、参照付きforeachには注意点があります。特に重要なのは、ループ終了後もループ変数が最後の要素を参照し続けるという点です。
この性質を理解していないと、配列の最後の要素が意図せず書き換わるバグにつながります。
foreachで参照を使う基本形
通常のforeachでは元の配列は変更されない
まず、通常のforeachの動きを確認します。
$array = [1, 2, 3];
foreach ($array as $value) {
$value *= 2;
}
print_r($array);
実行結果は次の通りです。
Array
(
[0] => 1
[1] => 2
[2] => 3
)
$valueを2倍にしていますが、元の$arrayは変わっていません。
これは、通常のforeach ($array as $value)では、$valueを変更しても元の配列要素には反映されないためです。
厳密には、PHP内部で毎回必ず実体コピーが発生しているという意味ではありません。
PHPにはコピーオンライトの仕組みがあるため、内部的なメモリ管理はもう少し複雑です。
ただ、コードを書く側の理解としては、通常のforeachでは「ループ変数を変更しても元の配列は変わらない」と覚えておけば問題ありません。
参照付きforeachでは元の配列を直接変更できる
元の配列を直接変更したい場合は、値変数の前に&を付けます。
$array = [1, 2, 3];
foreach ($array as &$value) {
$value *= 2;
}
unset($value);
print_r($array);
実行結果は次のようになります。
Array
(
[0] => 2
[1] => 4
[2] => 6
)
foreach ($array as &$value)と書くことで、$valueは配列要素への参照になります。
そのため、
$value *= 2;
と書くと、$valueだけでなく、元の$arrayの要素も変更されます。
参照付きforeachの最大の注意点
ループ後も最後の要素への参照が残る
参照付きforeachで最も注意すべき点は、ループが終わった後も、ループ変数が配列の最後の要素を参照したままになることです。
例えば、次のコードを見てください。
$array = [1, 2, 3, 4];
foreach ($array as &$value) {
$value *= 2;
}
// unset($value); をしていない
print_r($array);
この時点では、配列は次のように変更されています。
Array
(
[0] => 2
[1] => 4
[2] => 6
[3] => 8
)
ここまでは問題ありません。
しかし、foreachが終わった後も、$valueは$array[3]、つまり最後の要素を参照したままです。
この状態で、後続処理でも同じ$valueを使うと、意図しない書き換えが起こる可能性があります。
unsetしないと最後の要素が書き換わる
次のコードは、参照付きforeachで配列を変更した後、もう一度通常のforeachで配列を回しています。
$array = [1, 2, 3, 4];
foreach ($array as &$value) {
$value *= 2;
}
// unset($value); をしていない
foreach ($array as $value) {
// 何もしない
}
print_r($array);
一見すると、最初のループで配列の値を2倍にし、2回目のループでは何もしていないように見えます。
期待する結果は次のような配列です。
Array
(
[0] => 2
[1] => 4
[2] => 6
[3] => 8
)
しかし、実際には次のようになります。
Array
(
[0] => 2
[1] => 4
[2] => 6
[3] => 6
)
最後の要素が8ではなく、6になっています。
原因は、最初のforeachの後も$valueが最後の要素への参照を持ち続けているためです。
2回目のforeachで、
foreach ($array as $value) {
}
と書くと、各ループで$valueに値を代入しているように見えます。
しかし、$valueはまだ$array[3]への参照です。そのため、実際には次のような処理が起こります。
$value = $array[0]; // 実質的には $array[3] = $array[0]
$value = $array[1]; // 実質的には $array[3] = $array[1]
$value = $array[2]; // 実質的には $array[3] = $array[2]
$value = $array[3]; // 実質的には $array[3] = $array[3]
その結果、最後の要素が途中で上書きされてしまいます。
このバグは見た目では原因が分かりにくく、実務でも非常に厄介です。
参照付きforeachの後はunsetする
unsetでループ変数を破棄する
参照付きforeachを使った後は、基本的に直後にunset()を書きます。
foreach ($array as &$value) {
$value *= 2;
}
unset($value);
ここで重要なのは、unset($value)が配列の要素を削除する処理ではないという点です。
削除しているのは、ループ変数である$valueです。
unset($value);
によって、$valueという変数が現在のスコープから破棄されます。その結果、$valueに残っていた最後の配列要素への参照も切れます。
つまり、unset($value)は「配列を消す処理」ではなく、「最後の要素への参照が残らないように、ループ変数を破棄する処理」と考えると分かりやすいです。
安全な書き方
参照付きforeachを使う場合は、次の形を基本にすると安全です。
$array = [1, 2, 3, 4];
foreach ($array as &$value) {
$value *= 2;
}
unset($value);
foreach ($array as $value) {
echo $value . PHP_EOL;
}
unset($value)によって、最初のループで残っていた参照が切れています。
そのため、後続のforeachで同じ$valueという変数名を使っても、最後の要素が書き換わる問題は起きません。
ただし、保守性を考えるなら、後続のforeachでは別の変数名を使うのもおすすめです。
$array = [1, 2, 3, 4];
foreach ($array as &$value) {
$value *= 2;
}
unset($value);
foreach ($array as $currentValue) {
echo $currentValue . PHP_EOL;
}
unset()したうえで変数名も分けておくと、後からコードを読んだ人にも意図が伝わりやすくなります。
「参照渡し」という表現について
厳密には「参照付きforeach」が正確
PHPの記事では、foreach ($array as &$value)のことを「foreachの参照渡し」と表現することがあります。
実務上はこの言い方でも意味は通じます。
ただし、厳密には関数の引数として値を渡しているわけではないため、「参照渡し」よりも「参照付きforeach」や「参照による反復」と表現したほうが正確です。
例えば、以下のように表現できます。
foreachで値変数を参照にして回す
または、
参照付きforeachを使う
コードとしては、次の形です。
foreach ($array as &$value) {
// $valueは配列要素への参照
}
unset($value);
実務で起こりやすいバグ例
商品データの加工後に一覧表示するケース
ECサイトや管理画面では、商品一覧の配列を加工してから表示する処理がよくあります。
例えば、商品価格に税込価格を追加するケースです。
$products = [
['name' => 'Apple', 'price' => 100],
['name' => 'Banana', 'price' => 200],
['name' => 'Orange', 'price' => 300],
];
foreach ($products as &$product) {
$product['priceWithTax'] = (int) floor($product['price'] * 1.1);
}
// unset($product); を忘れている
foreach ($products as $product) {
echo $product['name'] . ':' . $product['priceWithTax'] . PHP_EOL;
}
このコードは危険です。
最初のforeachで$productは最後の要素、つまりOrangeの商品データを参照したままになります。
その後のforeachでも同じ$productを使っているため、最後の要素が意図せず書き換わる可能性があります。
正しくは、参照付きforeachの直後にunset($product)を書きます。
$products = [
['name' => 'Apple', 'price' => 100],
['name' => 'Banana', 'price' => 200],
['name' => 'Orange', 'price' => 300],
];
foreach ($products as &$product) {
$product['priceWithTax'] = (int) floor($product['price'] * 1.1);
}
unset($product);
foreach ($products as $product) {
echo $product['name'] . ':' . $product['priceWithTax'] . PHP_EOL;
}
このように書けば、最後の要素への参照が残らないため安全です。
ユーザー一覧にステータスを追加するケース
ユーザー一覧の配列を加工する場合も、参照付きforeachはよく使われます。
$users = [
['name' => 'Tanaka', 'active' => true],
['name' => 'Sato', 'active' => false],
];
foreach ($users as &$user) {
$user['status'] = $user['active'] ? '有効' : '無効';
}
unset($user);
print_r($users);
実行結果は次のようになります。
Array
(
[0] => Array
(
[name] => Tanaka
[active] => 1
[status] => 有効
)
[1] => Array
(
[name] => Sato
[active] =>
[status] => 無効
)
)
このように、配列内の各要素に新しいキーを追加したい場合、参照付きforeachは便利です。
ただし、この場合もunset($user)は忘れないようにしましょう。
参照付きforeachを使うべきケース
元の配列を直接変更したい場合
参照付きforeachが向いているのは、元の配列を直接変更したい場合です。
例えば、文字列の前後の空白を削除する処理です。
$names = [' Tanaka ', ' Sato ', ' Suzuki '];
foreach ($names as &$name) {
$name = trim($name);
}
unset($name);
print_r($names);
実行結果は次の通りです。
Array
(
[0] => Tanaka
[1] => Sato
[2] => Suzuki
)
各要素を直接加工する処理では、参照付きforeachを使うとシンプルに書けます。
配列内の連想配列を加工したい場合
配列の中に連想配列が入っている場合も、参照付きforeachは使いやすいです。
$posts = [
['title' => '記事A', 'views' => 120],
['title' => '記事B', 'views' => 80],
];
foreach ($posts as &$post) {
$post['isPopular'] = $post['views'] >= 100;
}
unset($post);
print_r($posts);
実行結果は次のようになります。
Array
(
[0] => Array
(
[title] => 記事A
[views] => 120
[isPopular] => 1
)
[1] => Array
(
[title] => 記事B
[views] => 80
[isPopular] =>
)
)
このように、配列内の各要素にフラグや計算結果を追加したい場合には、参照付きforeachが役立ちます。
参照付きforeachを避けたほうがよいケース
表示や集計だけなら参照は不要
配列の中身を表示するだけなら、参照付きforeachは不要です。
$items = ['A', 'B', 'C'];
foreach ($items as $item) {
echo $item . PHP_EOL;
}
このような処理で、
foreach ($items as &$item) {
echo $item . PHP_EOL;
}
unset($item);
と書く必要はありません。
参照を使うと、unset()忘れによるバグのリスクが増えます。
元の配列を変更しないのであれば、通常のforeachを使うべきです。
合計値を計算するだけなら参照は不要
合計値を計算する処理でも、参照付きforeachは不要です。
$items = [
['name' => 'Apple', 'price' => 100],
['name' => 'Banana', 'price' => 200],
['name' => 'Orange', 'price' => 300],
];
$total = 0;
foreach ($items as $item) {
$total += $item['price'];
}
echo $total;
この場合、元の$itemsを書き換える必要はありません。
したがって、参照付きforeachを使うメリットはほとんどありません。
新しい配列を作るだけなら参照は不要
元の配列を変更せず、新しい配列を作る場合も、参照付きforeachは不要です。
$prices = [100, 200, 300];
$pricesWithTax = [];
foreach ($prices as $price) {
$pricesWithTax[] = (int) floor($price * 1.1);
}
print_r($pricesWithTax);
この場合、元の$pricesはそのまま残ります。
新しい配列を作る処理では、通常のforeachのほうが安全で分かりやすいです。
参照付きforeachの代替方法
キーを使って元の配列を更新する
元の配列を変更したい場合でも、必ず参照付きforeachを使う必要はありません。
キーを使って元の配列を更新する方法もあります。
$prices = [100, 200, 300];
foreach ($prices as $key => $price) {
$prices[$key] = (int) floor($price * 1.1);
}
print_r($prices);
実行結果は次の通りです。
Array
(
[0] => 110
[1] => 220
[2] => 330
)
この方法なら、ループ変数が最後の要素への参照として残る問題は起きません。
参照付きforeachに慣れていない場合や、unset()忘れを避けたい場合は、キーを使って更新する方法も有効です。
連想配列でもキーを使って更新できる
多次元配列でも、キーを使って更新できます。
$users = [
['name' => 'Tanaka', 'score' => 80],
['name' => 'Sato', 'score' => 95],
];
foreach ($users as $key => $user) {
$users[$key]['passed'] = $user['score'] >= 90;
}
print_r($users);
このように、$users[$key]に対して代入すれば、元の配列を変更できます。
参照付きforeachと比べると少し記述は長くなりますが、参照が残る問題を避けられる点がメリットです。
array_mapで新しい配列を作る
配列を変換して新しい配列を作るなら、array_map()も選択肢になります。
$prices = [100, 200, 300];
$pricesWithTax = array_map(function ($price) {
return (int) floor($price * 1.1);
}, $prices);
print_r($pricesWithTax);
実行結果は次のようになります。
Array
(
[0] => 110
[1] => 220
[2] => 330
)
元の配列を置き換えたい場合は、次のようにも書けます。
$prices = [100, 200, 300];
$prices = array_map(function ($price) {
return (int) floor($price * 1.1);
}, $prices);
print_r($prices);
array_map()は、各要素を変換して新しい配列を返す処理に向いています。
ただし、複雑な連想配列を何段階も加工する場合は、通常のforeachのほうが読みやすいこともあります。
参照付きforeach中に配列構造を変更する場合の注意点
ループ中の追加・削除は避けたほうがよい
参照付きforeachの中で、対象の配列に要素を追加したり削除したりする処理は、できるだけ避けたほうが安全です。
例えば、次のようなコードです。
$array = [1, 2, 3];
foreach ($array as &$value) {
if ($value === 2) {
$array[] = 99;
}
}
unset($value);
このようなコードは、処理の流れが分かりにくくなります。
また、foreachは値で回す場合と参照で回す場合で、配列の変更に対する挙動が異なることがあります。
そのため、ループ中に配列構造を変更すると、読み手が挙動を予測しづらくなります。
値の変更と構造変更は分ける
安全に書くなら、値の変更と配列の追加・削除は分けるのがおすすめです。
例えば、追加したい要素を別の配列にためておき、ループ後にまとめて追加します。
$array = [1, 2, 3];
$itemsToAdd = [];
foreach ($array as $value) {
if ($value === 2) {
$itemsToAdd[] = 99;
}
}
$array = array_merge($array, $itemsToAdd);
print_r($array);
このように処理を分けると、コードの意図が明確になります。
参照付きforeachを使う場合は、基本的に「各要素の値を変更するだけ」に限定したほうが安全です。
関数内で参照付きforeachを使う場合
関数内でも参照は同じスコープ内に残る
関数内で参照付きforeachを使う場合も、unset()は意識しておくべきです。
function updatePrices(array $items): array
{
foreach ($items as &$item) {
$item['price'] *= 1.1;
}
foreach ($items as $item) {
// ここで意図しない書き換えが起こる可能性がある
}
return $items;
}
このコードでは、最初のforeachが終わった後も、$itemは最後の要素への参照を持っています。
その状態で、後続のforeachでも同じ$itemを使っているため、最後の要素が書き換わる可能性があります。
安全にするなら、次のように書きます。
function updatePrices(array $items): array
{
foreach ($items as &$item) {
$item['price'] *= 1.1;
}
unset($item);
foreach ($items as $item) {
// 安全
}
return $items;
}
関数内であっても、同じ関数スコープの後続処理では参照が影響します。
関数がすぐ終了する場合でもunsetしておくと安全
次のように、参照付きforeachの直後に関数が終了する場合、実害が出ないこともあります。
function doubleValues(array $array): array
{
foreach ($array as &$value) {
$value *= 2;
}
return $array;
}
この場合、関数が終了すればローカル変数も破棄されるため、後続処理で$valueを使って配列が壊れる問題は起こりにくいです。
ただし、後から処理が追加される可能性を考えると、次のように書いておくほうが安全です。
function doubleValues(array $array): array
{
foreach ($array as &$value) {
$value *= 2;
}
unset($value);
return $array;
}
参照付きforeachを書いたら、その直後にunset()を書くというルールにしておくと、保守時の事故を防ぎやすくなります。
参照付きforeachを書くときのチェックポイント
元の配列を変更する必要があるか確認する
参照付きforeachは、元の配列を直接変更したい場合に使います。
表示、集計、検索、判定だけであれば、通常のforeachで十分です。
foreach ($items as $item) {
// 表示・集計・判定など
}
元の配列を変更しない処理で参照を使うと、不要なリスクが増えます。
ループ直後にunsetしているか確認する
参照付きforeachを使ったら、直後にunset()を書きます。
foreach ($items as &$item) {
// 元の配列を変更する処理
}
unset($item);
このunset()は、最後の要素への参照を残さないための重要な処理です。
後続処理で同じ変数名を使っていないか確認する
unset()していない状態で、同じ変数名を後続のforeachに使うと危険です。
foreach ($items as &$item) {
// 加工
}
// unset($item); がない
foreach ($items as $item) {
// 危険
}
安全にするなら、次のようにします。
foreach ($items as &$item) {
// 加工
}
unset($item);
foreach ($items as $currentItem) {
// 安全
}
unset()を入れたうえで、変数名も分けるとより安心です。
ループ中に配列の追加・削除をしていないか確認する
参照付きforeachの中で、対象配列の要素を追加・削除すると、処理が分かりにくくなります。
foreach ($array as &$value) {
if ($value === 2) {
$array[] = 99;
}
}
unset($value);
このような処理は、可能であれば分けて書くほうが安全です。
実務でのおすすめルール
基本ルール
参照付きforeachを使う場合は、次の形を基本にすると安全です。
foreach ($array as &$value) {
// 元の配列要素を直接変更する
}
unset($value);
この書き方を徹底するだけで、最後の要素が意図せず書き換わるバグをかなり防げます。
より安全な書き方
後続処理がある場合は、変数名も分けるとより安全です。
foreach ($array as &$value) {
$value = trim($value);
}
unset($value);
foreach ($array as $currentValue) {
echo $currentValue . PHP_EOL;
}
unset()によって参照を切り、さらに別の変数名を使うことで、コードの意図が明確になります。
参照を使わない選択肢も検討する
単純な更新であれば、キーを使った代入も検討できます。
foreach ($array as $key => $value) {
$array[$key] = trim($value);
}
この方法なら、参照付きforeach特有の「最後の要素への参照が残る」問題は起きません。
参照付きforeachは便利ですが、必ず使うべきものではありません。
読みやすさ、安全性、処理内容に応じて使い分けるのが大切です。
まとめ
参照付きforeachではunsetが重要
PHPのforeachでは、値変数の前に&を付けることで、配列要素を参照として扱えます。
foreach ($array as &$value) {
$value *= 2;
}
unset($value);
この書き方をすると、ループ内で$valueを変更したときに、元の配列要素も直接変更されます。
ただし、参照付きforeachの後は、ループ変数が最後の要素への参照を持ち続けます。
そのため、直後にunset($value)を書いて、ループ変数を破棄するのが安全です。
参照付きforeachは必要なときだけ使う
参照付きforeachは、元の配列を直接変更したい場合に便利です。
一方で、表示、集計、新しい配列の作成だけなら、通常のforeachやarray_map()で十分です。
実務では、次のように判断するとよいです。
| やりたいこと | おすすめの書き方 |
|---|---|
| 配列を表示する | 通常のforeach |
| 合計を計算する | 通常のforeach |
| 新しい配列を作る | 通常のforeachまたはarray_map() |
| 元の配列を直接変更する | 参照付きforeachまたはキー代入 |
| 参照によるバグを避けたい | キー代入 |
特に初心者のうちは、参照付きforeachを使うたびに、次の3点を確認すると安全です。
| チェック項目 | 内容 |
|---|---|
| 元の配列を直接変更する必要があるか | 不要なら参照は使わない |
ループ直後にunset()しているか | 参照を残さないために重要 |
| 後続処理で同じ変数名を使っていないか | unset()なしだと危険 |
PHPの参照付きforeachは便利ですが、扱いを間違えると分かりにくいバグを生みます。
基本は、参照付きforeachを使ったら直後にunset()することです。
foreach ($array as &$value) {
// 元の配列要素を変更する
}
unset($value);
このルールを守るだけで、参照付きforeachによる多くのトラブルを防げます。
以上、PHPのforeachで参照渡しをしたい時の注意点についてでした。
最後までお読みいただき、ありがとうございました。









