In PHP 7 and later, what happens when you add or change array elements inside a foreach loop over that same array?
answer
- by value: iterates the array as it was
- changes land in the array, not the loop
- by reference: appended elements are visited
- foreach ignores the internal array pointer
- PHP 7.0 changed this
basics
~20 sBy value, foreach iterates the array as it stood when the loop began, so changes made in the body are not visited. By reference, it follows the live array and visits appended elements. Since PHP 7.0 foreach never moves the internal pointer.
solid answer
~50 sSince **PHP 7.0**, a `foreach` **by value** works on the array as it was when the loop started; the manual describes it as operating on a copy. Writes inside the body, such as `$parcels[] = $next` or `$parcels[$k] = ...`, change the array variable, but the loop keeps visiting the original elements, so appended items are not visited and changed values are not seen. A `foreach` **by reference** iterates the live array: changes to later elements are seen, and elements appended during the loop are visited too, which is how an unguarded by-reference loop can run for a long time. Also since PHP 7.0, `foreach` does not move the internal array pointer, so `current()` and `key()` inside the loop no longer track it. Changing an array while iterating it is legal but hard to read; building a new array or collecting changes for after the loop is clearer.
code
php · 19 lines<?php
declare(strict_types=1);
$events = ['picked_up', 'arrived_hub'];
foreach ($events as $event) {
if ($event === 'arrived_hub') {
$events[] = 'customs_cleared'; // added to $events, not visited
}
echo $event, PHP_EOL; // picked_up, arrived_hub
}
$queue = ['arrived_hub'];
foreach ($queue as &$item) {
if ($item === 'arrived_hub') {
$queue[] = 'customs_cleared'; // visited on the next pass
}
echo $item, PHP_EOL; // arrived_hub, customs_cleared
}
unset($item);go deeper
Know that a by-value foreach does not see changes made to the array inside the loop, while the array itself still keeps them.
Explain the PHP 7 rules: by value iterates the original array, by reference follows the live array and visits appended elements, and the internal pointer is untouched.
Spot loops that rely on seeing their own appends or on current() and next(), and rewrite them as explicit queues or two-pass collect-then-apply code.
Weigh in-place mutation during iteration against immutable transformation in shared code, where readability and bounded loops matter more than saving a copy.
## The question behind the question Code sometimes modifies the array it is iterating: appending follow-up items to a work list, rewriting a value, removing processed entries. Whether the loop "sees" those changes depends on how the `foreach` iterates. PHP 7.0 redefined the rules, and they are worth knowing precisely. ## foreach by value: the array as it was In the default by-value form, `foreach ($parcels as $parcel)`, PHP 7 and later iterate over **the array value that existed when the loop began**. The manual's migration notes put it as "foreach will now operate on a copy of the array being iterated". In practice the engine keeps its own hold on that array value, and PHP's copy-on-write handling separates the variable's array from the one being iterated as soon as the body writes to it. The effect: - **Appending** (`$parcels[] = $x`) grows `$parcels`, but the loop does not visit the new element. - **Changing a later element** (`$parcels[5] = 'x'`) is not seen when the loop reaches index 5; it still yields the old value. - **Unsetting** an element does not stop the loop from visiting it. After the loop, `$parcels` holds every change; only the iteration ignored them. ## foreach by reference: the live array With `foreach ($parcels as &$parcel)`, the loop iterates the **actual array** and tracks changes to it: - values changed ahead of the loop position are seen when the loop gets there; - elements **appended during the loop are visited**, as the PHP 7.0 migration guide demonstrates with `$array[1] = 1;` inside a loop over `[0]`, which produces two iterations. That last point is what makes a by-reference work-list loop possible, and also what makes an unguarded one dangerous: if every iteration appends, the loop never ends until memory runs out. ## The internal array pointer Every PHP array has an **internal pointer** used by `current()`, `key()`, `next()` and `reset()`. Before PHP 7, `foreach` moved that pointer as it went, so `current($array)` inside a loop changed on every iteration. Since **PHP 7.0**, `foreach` does not touch the internal pointer at all. Code that mixed `foreach` with `current()` or `next()` to peek ahead broke in that release; the manual's migration example shows `current()` returning the first element on every iteration. | Behaviour | PHP 5 | PHP 7.0 and later | |---|---|---| | `foreach` moves the internal pointer | yes | no | | by value sees changes made in the body | not guaranteed | no: iterates the array as it was | | by reference visits elements appended in the body | no, in the manual's example | yes | ## Objects Iterating a plain object (one that does not implement `Traversable`) walks its visible properties, and PHP 7 gave it the same live behaviour as by-reference array iteration, so properties added during the loop can be visited. Objects that implement the iterator interfaces follow their own rules, which belong with those interfaces. ## Practical guidance 1. **Do not modify the array you iterate by value and expect the loop to notice.** If you need the new items processed, use a queue: `while ($queue !== []) { $item = array_shift($queue); ... }`. (Note that `array_shift()` re-indexes the array, which is O(n) per call; for large queues, prefer an index or an SPL queue.) 2. **Collect changes** into a second array and merge them after the loop; it reads better and has no surprises. 3. **If you must use a by-reference loop that appends, add a hard cap** on iterations, and `unset()` the loop variable afterwards. 4. **Never rely on `current()` or `next()` inside `foreach`**; take the key from `foreach ($arr as $key => $value)` instead. ## A worked example A parcel tracker loads a list of status events and, while scanning, discovers a missing "customs cleared" event that it appends: ```php foreach ($events as $event) { if ($event === 'arrived_hub') { $events[] = 'customs_cleared'; } } ``` The by-value loop never visits `customs_cleared`, although `$events` contains it afterwards. Written by reference, the loop would visit it, and if `customs_cleared` also triggered an append, it would keep going.
- Inside a foreach by value, does unset($arr[$k]) on a later key stop that element from being visited?No. A by-value `foreach` iterates the array as it was when the loop began, so the element is still visited with its old value, even though `$arr` no longer contains it after the `unset()`. Only a by-reference loop follows removals in the live array.
- Why did code that called current($arr) inside foreach break when moving to PHP 7?Before PHP 7, `foreach` advanced the array's internal pointer, so `current()` inside the loop tracked the loop position. PHP 7.0 stopped `foreach` from touching the internal pointer, so `current()` keeps returning whatever the pointer pointed at before the loop, usually the first element. Take the key and value from `foreach` itself instead.
saying these in an interview costs you the question
- Says a foreach by value visits elements appended inside the loop body.
- Believes changes made to the array inside a by-value foreach are lost after the loop.
- Thinks foreach by reference ignores elements appended during the loop.
- Claims foreach still advances the internal array pointer in PHP 8.
- Says modifying an array inside foreach throws an error in PHP 8.