Question

How PHP foreach Actually Works with Arrays

phploopsforeachiterationphp-internals

Question

I understand what foreach is, what it does, and how to use it. My question is specifically about how foreach works internally in PHP.

For a long time, I assumed that foreach iterated over the original array itself. Later, I found many explanations saying that it works on a copy of the array instead, so I assumed that was the full story. However, after some experiments, that explanation seems incomplete.

Assume we start with this array:

$array = array(1, 2, 3, 4, 5);

Test case 1

foreach ($array as $item) {
    echo "$item\n";
    $array[] = $item;
}

print_r($array);

/* Output in loop:    1 2 3 4 5
   $array after loop: 1 2 3 4 5 1 2 3 4 5 */

This suggests that foreach is not iterating directly over the live array structure, because otherwise the loop would keep growing forever.

Test case 2

foreach ($array as $key => $item) {
    $array[$key + 1] = $item + 2;
    echo "$item\n";
}

print_r($array);

/* Output in loop:    1 2 3 4 5
   $array after loop: 1 3 4 5 6 7 */

This again suggests that the values being iterated are not simply read back from the array after modification.

Test case 3

var_dump(each($array));

foreach ($array as $item) {
    echo "$item\n";
}

var_dump(each($array));

/* Output
array(4) {
  [1]=>
  int(1)
  ["value"]=>
  int(1)
  [0]=>
  int(0)
  ["key"]=>
  int(0)
}
1
2
3
4
5
bool(false)
*/

This seems to show that after foreach, the array's internal pointer is at the end.

Test case 4

foreach ($array as $key => $item) {
    echo "$item\n";
    each($array);
}

/* Output: 1 2 3 4 5 */

Test case 5

foreach ($array as $key => $item) {
    echo "$item\n";
    reset($array);
}

/* Output: 1 2 3 4 5 */

These tests suggest that changing the array pointer manually inside the loop does not affect iteration.

So what is foreach really doing internally?

  • Does it iterate over a copy of the array?
  • Does it interact with the original array’s internal pointer?
  • Why does the pointer appear to end up at the end after the loop?
  • Can pointer functions such as each() or reset() ever affect a foreach loop’s behavior?

Short Answer

By the end of this page, you will understand the real model behind PHP foreach: it does not simply mean “loop over a full copy of the array,” and it also does not behave like while (each(...)). You will learn how foreach behaves when iterating by value vs by reference, how array modifications affect the loop, why internal pointer functions can be misleading, and what rules are safe to follow in real PHP code.

Concept

foreach in PHP is a dedicated iteration construct for arrays and traversable objects. The key idea is:

  • foreach maintains its own iteration state.
  • It is not controlled by reset(), next(), each(), or the array's user-visible internal pointer.
  • It may use PHP's internal array structure efficiently rather than making a naive element-by-element PHP-level copy.

A common beginner explanation is that foreach “loops over a copy of the array.” That explanation is useful in some cases, but it is not the full internal truth.

What foreach really guarantees

When you write:

foreach ($array as $value) {
    // ...
}

PHP determines how to iterate through the elements in a way that is stable for the loop. In normal by-value iteration:

  • the loop visits the elements that were part of the iteration sequence,
  • appending or modifying the array during the loop usually does not make the loop continue over newly added elements,
  • changing the traditional array pointer with functions like reset() does not drive the loop.

When you iterate by reference:

foreach ($array as &$value) {
    // ...
}

behavior changes, because now the loop variable is linked directly to the actual array element.

Why this matters

In real programs, developers often:

  • filter arrays,
  • transform values,
  • validate request data,
  • build query parameters,
  • normalize configuration arrays.

If you misunderstand foreach, you may accidentally:

  • expect new elements to be visited when they are not,
  • corrupt data by using references incorrectly,
  • assume reset() or next() can control a foreach loop,
  • introduce bugs when mutating an array during iteration.

Historical note

Older PHP versions had details that caused confusion, especially around internal pointers and copy-on-write behavior. The safest modern understanding is:

  • foreach has its own iteration mechanism,
  • it is separate from pointer-based array functions,
  • modifying the iterated array during the loop can produce different results depending on whether iteration is by value or by reference.

So the best mental rule is not “foreach always uses a copy,” but rather:

foreach creates a stable iteration context, and PHP handles the underlying array efficiently behind the scenes.

Mental Model

Think of foreach like reading names from an attendance list.

By value

When iterating by value, it is like the teacher starts class with a prepared list of students to call:

  • if someone writes extra names on the whiteboard during class, the teacher usually does not start calling those new names,
  • if someone changes the seating position, it does not change the prepared calling order.

By reference

When iterating by reference, it is more like the teacher is not only reading the list but also editing the official roster directly as they go.

That means:

  • changes affect the real data immediately,
  • the loop variable stays connected to the actual array slot,
  • mistakes can have side effects after the loop finishes.

Internal pointer functions

Functions like reset() and each() are like moving a separate bookmark in the same book.

foreach is not reading based on that bookmark. It has its own place-tracking system. So moving the bookmark does not usually change what foreach reads next.

Take Quiz

Syntax and Examples

Basic syntax

Iterate by value

foreach ($array as $value) {
    echo $value;
}

Iterate with keys and values

foreach ($array as $key => $value) {
    echo $key . ': ' . $value . PHP_EOL;
}

Iterate by reference

foreach ($array as &$value) {
    $value *= 2;
}
unset($value); // important

Example 1: normal by-value iteration

$numbers = [1, 2, 3];

foreach ($numbers as $n) {
    echo $n . PHP_EOL;
    $numbers[] = $n * 10;
}

print_r($numbers);

Output:

1
2
3
Array
(
    [0] => 1
    [1] => 2
    [2] => 3
    [3] => 10
    [4] => 20
    [5] => 30
)

What this shows

  • The loop prints only the original 1, 2, 3.
  • The appended values are added to the array.
  • The loop does not keep iterating forever.

Example 2: iterate by reference

$numbers = [1, 2, 3];

foreach ($numbers as &$n) {
    $n *= 2;
}
unset($n);

print_r($numbers);

Output:

Array
(
    [0] => 2
    [1] => 4
    [2] => 6
)

What this shows

  • $n is a reference to each real array element.
  • Changing $n changes the array itself.
  • unset($n) is important so $n does not remain a reference to the last element.

Example 3: pointer functions do not control foreach

$numbers = [10, 20, 30];
reset($numbers);

foreach ($numbers as $n) {
    echo $n . PHP_EOL;
    next($numbers);
}

The loop still prints all values in normal foreach order. Calling next() here does not drive the loop.

Step by Step Execution

Consider this example:

$items = [1, 2, 3];

foreach ($items as $key => $value) {
    echo "$key => $value" . PHP_EOL;
    $items[] = $value + 100;
}

print_r($items);

Step-by-step trace

Before the loop

$items = [1, 2, 3];

The array contains three elements.

First iteration

  • key = 0
  • value = 1
  • output: 0 => 1
  • then this runs:
$items[] = 101;

Now the real array becomes:

[1, 2, 3, 101]

But the loop continues according to its own iteration state.

Second iteration

  • key = 1
  • value = 2
  • output: 1 => 2
  • append 102

Array becomes:

[1, 2, 3, 101, 102]

Third iteration

  • key = 2
  • value = 3
  • output: 2 => 3
  • append 103

Array becomes:

[1, 2, 3, 101, 102, 103]

Loop ends

The loop stops after the original iteration sequence is exhausted.

Final result

Array
(
    [0] => 1
    [1] => 2
    [2] => 3
    [3] => 101
    [4] => 102
    [5] => 103
)

Key lesson

The array does change during the loop, but foreach by value does not simply chase every newly appended element. That is why appending inside the loop does not create an infinite loop in this example.

Real World Use Cases

foreach is one of the most common constructs in PHP because arrays are used everywhere.

1. Processing request data

foreach ($_POST as $field => $value) {
    echo $field . ': ' . trim($value) . PHP_EOL;
}

Used for:

  • sanitizing form input,
  • validating required fields,
  • building error lists.

2. Formatting API responses

foreach ($users as $user) {
    $result[] = [
        'id' => $user['id'],
        'name' => strtoupper($user['name'])
    ];
}

Used when transforming database or API data into response payloads.

3. Updating configuration values

foreach ($config as $key => $value) {
    if ($value === null) {
        $config[$key] = 'default';
    }
}

Used for normalizing settings before an application starts.

4. Batch file or job processing

foreach ($jobs as $job) {
    processJob($job);
}

Used in scripts, queues, cron jobs, and import/export tools.

5. Building HTML or CLI output

foreach ($messages as $message) {
    echo '- ' . $message . PHP_EOL;
}

Used in templates, reports, logs, and console commands.

Take Quiz

Real Codebase Usage

In real codebases, foreach is usually used in predictable, low-surprise ways.

Common patterns

Guard clauses before looping

if (empty($users)) {
    return [];
}

foreach ($users as $user) {
    // process user
}

This avoids unnecessary work and makes intent clear.

Validation during iteration

$errors = [];

foreach ($input as $field => $value) {
    if ($value === '') {
        $errors[$field] = 'This field is required.';
    }
}

Building a new array instead of mutating the current one

$clean = [];

foreach ($records as $record) {
    $clean[] = trim($record);
}

This is often clearer and safer than changing the iterated array.

Updating in place with references only when needed

foreach ($prices as &$price) {
    $price = round($price, 2);
}
unset($price);

Use this only when you intentionally want to modify the original array.

Prefer explicit transformations

In modern PHP, developers sometimes choose array_map(), array_filter(), or array_reduce() for transformation-heavy code. But foreach is still preferred when:

  • multiple statements are needed,
  • validation and branching are involved,
  • readability matters more than compactness.

Best practice

In production code, avoid changing the structure of the same array you are currently iterating unless you are certain about the behavior. A safer pattern is:

$newItems = [];

foreach ($items as $item) {
    $newItems[] = transform($item);
}

instead of appending into $items while looping over $items.

Common Mistakes

1. Assuming foreach is controlled by reset() or next()

Broken assumption:

$numbers = [1, 2, 3];

foreach ($numbers as $n) {
    next($numbers);
    echo $n;
}

A beginner may expect this to skip values. It does not work that way.

Avoid it

Use foreach alone, or use pointer-based functions separately. Do not mix the two to control iteration.

2. Modifying the array structure and expecting intuitive results

$numbers = [1, 2, 3];

foreach ($numbers as $n) {
    $numbers[] = $n;
}

This changes the array, but the loop behavior may not match your mental model.

Avoid it

If you need a growing result, write to a separate array:

$result = [];

foreach ($numbers as $n) {
    $result[] = $n;
}

3. Forgetting unset() after iterating by reference

Broken code:

$numbers = [1, 2, 3];

foreach ($numbers as &$n) {
    $n *= 2;
}

foreach ($numbers as $n) {
    // unexpected behavior can happen here
}

Why this happens

After the first loop, $n still references the last array element.

Correct version

$numbers = [1, 2, 3];

foreach ($numbers as &$n) {
    $n *= 2;
}
unset($n);

foreach ($numbers as $n) {
    echo $n . PHP_EOL;
}

4. Expecting by-value iteration to update the array

Broken expectation:

$numbers = [1, 2, 3];

foreach ($numbers as $n) {
    $n *= 2;
}

print_r($numbers);

The original array is unchanged.

Why

$n is just a copy of each value.

Correct approach

foreach ($numbers as &$n) {
    $n *= 2;
}
unset($n);

5. Using each() in modern PHP code

each() is deprecated and removed in modern PHP versions.

Avoid it

Use foreach for iteration, or explicit index-based loops when needed.

Comparisons

ConceptHow it worksAffects original array values?Uses internal pointer functions?Best use case
foreach ($array as $value)Iterates by valueNoNoReading or transforming values safely
foreach ($array as &$value)Iterates by referenceYesNoUpdating array values in place
for ($i = 0; $i < count($array); $i++)Index-based loopYes, if you assign backNoNumeric arrays when index control matters
while (list($k, $v) = each($array))Pointer-driven iterationYes, depending on codeYesLegacy code only
array_map()Returns transformed arrayNo, unless assignedNoSimple value transformation

foreach vs pointer-based iteration

foreach

  • clearer,
  • safer,
  • independent of reset() and next(),
  • recommended in modern PHP.

pointer-based functions

  • depend on internal pointer position,
  • easier to misuse,
  • mostly seen in old code.

By value vs by reference

StyleLoop variable containsChanges to loop variable affect source array?
as $valuea value copyNo
as &$valuea reference to the real elementYes
Take Quiz

Cheat Sheet

Core rules

  • foreach has its own iteration state.
  • It does not rely on reset(), next(), or each() to move through elements.
  • By-value foreach does not modify the source array unless you explicitly assign into the array.
  • By-reference foreach lets you modify the original elements.
  • After by-reference iteration, always call unset($value).

Syntax

foreach ($array as $value) {
    // read values
}

foreach ($array as $key => $value) {
    // read keys and values
}

foreach ($array as &$value) {
    // modify values in place
}
unset($value);

Safe patterns

$result = [];
foreach ($items as $item) {
    $result[] = transform($item);
}
foreach ($items as &$item) {
    $item = trim($item);
}
unset($item);

Things to avoid

  • Mixing foreach with each() or reset() to control the loop
  • Appending to the same array and expecting new elements to always be iterated
  • Forgetting unset() after &$value
  • Assuming $value changes the original array in by-value mode

Quick memory rule

  • as $value = read a value
  • as &$value = edit the real element

FAQ

Does PHP foreach loop over a copy of an array?

Not in the simple beginner sense of “PHP duplicates the whole array and loops over that copy every time.” A better explanation is that foreach creates its own stable iteration context.

Does reset() affect a foreach loop?

No. foreach does not use reset() or next() to decide which element comes next.

Why can I change the array inside foreach without an infinite loop?

In normal by-value iteration, foreach does not automatically keep extending its iteration to include everything you append during the loop.

When should I use foreach by reference?

Use it when you intentionally want to modify the original array values in place.

Why do I need unset($value) after foreach (&$value)?

Because the loop variable remains a reference to the last element. If you reuse that variable later, you may accidentally modify the array.

Can foreach be used with objects?

Yes. It works with arrays and objects that are traversable.

Is each() still recommended with foreach?

No. each() is legacy functionality and should not be used in modern PHP code.

Take Quiz

Mini Project

Description

Build a small PHP script that demonstrates the difference between iterating an array by value and by reference. This project is useful because many real bugs come from accidentally assuming that foreach updates the original array or from forgetting to clear a reference after the loop.

Goal

Create a script that shows three behaviors: reading values with foreach, modifying values by reference, and safely building a new transformed array.

Requirements

  • Start with a numeric array containing at least three values.
  • Use one foreach loop by value and show that the original array does not change.
  • Use one foreach loop by reference and update the original array.
  • Build a separate result array using another foreach loop.
  • Print the arrays after each step so the behavior is visible.
Take Quiz

Keep learning

Related questions

View more