skip to content

In PHP, why does a foreach by value that reuses the variable of an earlier foreach by reference corrupt the last element?

level: middleimportance: must knowfreq 56%

answer

  1. the loop variable outlives the loop
  2. still a reference to the last element
  3. second loop writes into that slot
  4. second-to-last value copied onto last
  5. unset($value) after the loop

basics

~20 s

After foreach ($arr as &$value), $value remains a reference to the last element. A later by-value foreach reusing $value writes each element into that slot, leaving it equal to the second-to-last value. unset($value) after the first loop prevents it.

solid answer

~40 s

PHP has **no block scope**, so the loop variable of `foreach ($parcels as &$parcel)` still exists after the loop, and it is still a **reference to the last element**. If the next loop reuses the name by value, `foreach ($parcels as $parcel)`, each iteration assigns the current element to `$parcel`, which writes straight into the last slot of the array. On the final iteration the last slot receives its own current value, which by then is a copy of the second-to-last element. So `[1, 2, 3, 4]` doubled by reference becomes `[2, 4, 6, 8]`, and after the second loop it reads `[2, 4, 6, 6]`. The fix the manual recommends is `unset($parcel);` right after the by-reference loop; renaming the second loop's variable also avoids it.

code

php · 15 lines
php
<?php
declare(strict_types=1);

$trackingIds = [' pk-1 ', ' pk-2 ', ' pk-3 '];

foreach ($trackingIds as &$id) {
    $id = strtoupper(trim($id));
}
unset($id); // without this line, the next loop rewrites the last element

foreach ($trackingIds as $id) {
    echo $id, PHP_EOL; // PK-1, PK-2, PK-3
}

var_dump($trackingIds); // ['PK-1', 'PK-2', 'PK-3']

go deeper

for a junior

Remember the rule: after a foreach by reference, call unset() on the loop variable before reusing its name.

for a middle

Trace why the last element ends up equal to the second-to-last one: no block scope, a live reference, and by-value assignments writing through it.

for a senior

Recognise the duplicated-last-item symptom in production data and track it to an earlier by-reference loop; prefer array_map or keyed writes in shared code.

for a principal

Decide on team rules for references in loops, such as banning by-reference foreach outside small functions and enforcing unset through review or static analysis.

## The setup A `foreach` can iterate **by value** or **by reference**: - `foreach ($statuses as $status)` assigns a copy of each element to `$status`; changing `$status` does not touch the array. - `foreach ($statuses as &$status)` makes `$status` a **reference** to each element in turn, so `$status = strtoupper($status);` rewrites the array in place. The trap appears when a by-reference loop is followed by another loop that reuses the same variable name. ## Step by step ```php $arr = [1, 2, 3, 4]; foreach ($arr as &$value) { $value = $value * 2; } // $arr is [2, 4, 6, 8], and $value is still a reference to $arr[3] foreach ($arr as $value) { // each assignment to $value writes into $arr[3] } // $arr is [2, 4, 6, 6] ``` Trace the second loop, where every assignment to `$value` lands in `$arr[3]`: | Iteration | Element read | `$arr[3]` afterwards | |---|---|---| | 1 | `$arr[0]` = 2 | 2 | | 2 | `$arr[1]` = 4 | 4 | | 3 | `$arr[2]` = 6 | 6 | | 4 | `$arr[3]`, now 6 | 6 | By the last iteration the last element has already been overwritten with the second-to-last value, so it "reads itself" and stays 6. The manual documents exactly this output. `var_dump($arr)` even shows the last element as `&int(6)`, the `&` marking that a reference to it is still alive. ## Why PHP behaves this way Two ordinary rules combine: 1. **No block scope.** Variables in PHP are scoped to the function (or the file's global scope), not to the loop body. The loop variable is not destroyed when the loop ends. 2. **Assignment to a reference writes through.** Once `$value` is bound to `$arr[3]`, any plain assignment `$value = ...` changes `$arr[3]`. A by-value `foreach` performs exactly such an assignment on every iteration. Nothing here is a bug in the engine; the manual carries a warning about it on the `foreach` page. ## Fixing and avoiding it - **`unset($value);` immediately after the by-reference loop.** `unset()` breaks the binding between the name and the element without touching the array. This is the manual's recommendation. - **Use different names** for by-reference and by-value loops, so a later loop never writes through an old reference. - **Avoid by-reference loops when a transformation will do.** `array_map()` or building a new array returns a fresh value and leaves no reference behind. - **Wrap the by-reference loop in a small function**, so the reference dies with the function's scope. ## Where it bites in real code The bug rarely looks as tidy as the example. Typical forms: - a normalisation pass (`trim` every tracking number by reference) followed, dozens of lines later, by a reporting loop that reuses `$parcel`; - a template or view that iterates the same array twice after a controller modified it by reference; - nested loops where the inner loop is by reference and the outer loop later reuses the name. The symptom is always the same and very recognisable: **the last item is a duplicate of the one before it**, and only after a certain code path ran. When you see that in a report, search for a `&$` in an earlier `foreach`. ## Spotting it in code review A reviewer can catch the pattern mechanically: 1. Find every `foreach` whose value variable starts with `&`. 2. Check that the next statement after the loop is `unset()` of that variable, or that the loop sits alone in a small function. 3. If neither holds, search the rest of the scope for the same variable name being assigned, including in a later `foreach`, a `list()` or a plain assignment. Any plain assignment to the stale name corrupts the last element, not only a second loop; the loop is simply the most common way to write one without noticing. ## Related rules worth knowing - References are one of PHP's variable features in general; what matters for this question is that a `foreach` by reference creates one per iteration and leaves the last one bound. - Iterating a literal array by reference, `foreach ([1, 2, 3] as &$v)`, is allowed; there is simply no named array for the trap to corrupt afterwards.

  • Why does unset($value) fix the problem without removing the last element from the array?
    `unset()` on a variable destroys that variable name, not the value it refers to. After `unset($value)` the name no longer points at `$arr[3]`, so the element keeps its value and its other holder, the array, is untouched. A later loop then creates a fresh, unrelated `$value`.
  • Does the trap also happen if the second loop uses a different variable name?
    No. The corruption only happens because the second loop assigns to the variable that is still a reference into the array. With a different name, the old `$value` reference stays alive but nothing writes to it, so the array is safe. Unsetting it is still good hygiene, since any later `$value = ...` would hit the array.
  • How can you transform every element without a by-reference loop at all?
    Build a new array: `$labels = array_map(strtoupper(...), $labels);` or a by-value `foreach` that writes `$result[$key] = ...`. Both leave no reference behind. For in-place edits by key, `foreach ($arr as $key => $value) { $arr[$key] = ...; }` also avoids references.

saying these in an interview costs you the question

  • Believes the foreach loop variable is destroyed when the loop ends.
  • Says the second loop corrupts every element, not just the last one.
  • Thinks unset($value) after the loop deletes the last array element.
  • Blames the second by-value loop for making a reference, not the first loop.
  • Claims PHP 8 removed the trap by giving foreach variables block scope.