PHP foreach Loop

The foreach loop is PHP’s purpose-built tool for walking through every element of an array or any traversable object, without you having to manage a counter or an index by hand. It automatically visits each item in order, optionally handing you both the key and the value, which makes it the natural choice whenever you need to process a whole collection of data — a list of usernames, a set of product prices, rows pulled from a database, or the fields of a submitted form. Because it hides the bookkeeping that a classic for loop requires, foreach code tends to be shorter, safer, and easier to read. Mastering it — including its by-reference form and its well-known quirks — is essential for writing correct, idiomatic PHP.

Overview: How foreach Works

foreach only works on two kinds of things: arrays, and objects that are traversable — meaning they implement PHP’s built-in Iterator or IteratorAggregate interface, or are Generator objects produced by a generator function. A plain object with only public properties can also be iterated (PHP walks its visible properties as if they were an associative array), but scalar values like integers, booleans, or null cannot — attempting that raises Warning: foreach() argument must be of type array|object and the loop body simply never runs.

For a plain array, PHP resets an internal cursor to the first element and then, on each pass, reads the current key and value, assigns them to your loop variables, advances the cursor, and repeats until every element has been visited. When you iterate by value (the default form), PHP relies on its copy-on-write engine: the array is not physically duplicated unless something forces a separation, so iterating even a large array is memory-efficient. The trade-off is that your loop variable is a disposable copy — changing it inside the loop never touches the original array. When you iterate by reference using &, PHP instead binds your loop variable directly to each array slot, so any assignment you make inside the loop is written straight back into the array as iteration proceeds.

For objects, foreach delegates to the Iterator protocol: PHP calls rewind() once, then repeatedly checks valid(), reads current() and key(), executes your loop body, and finally calls next(). This single mechanism is why classes like ArrayIterator, database result objects, and generator functions all plug into the exact same foreach syntax you use for ordinary arrays.

Syntax

foreach comes in several forms, all built around the same keyword:

<?php
foreach ($array as $value) {
    // use $value
}

foreach ($array as $key => $value) {
    // use $key and $value
}

foreach ($array as &$value) {
    // modifies $array directly
}
unset($value); // always unset after a by-reference loop

foreach ($array as [$first, $second]) {
    // destructures each sub-array
}
Part Meaning
$array Any array, or an object implementing Iterator/IteratorAggregate, or a Generator.
as $value Assigns each element’s value, in order, to $value for the loop body.
$key => $value Also exposes the current element’s key (numeric index or string key) as $key.
&$value Binds $value as a reference to the actual array slot, so assignments mutate the original array.
[$a, $b] List/array destructuring — unpacks a nested array (or matching keys) into separate variables on every iteration.

Inside templates you may also see the alternative colon syntax, which avoids curly braces:

<?php
foreach ($array as $value):
    echo $value;
endforeach;

Examples

Example 1: Basic value iteration

<?php
$fruits = ["apple", "banana", "cherry"];

foreach ($fruits as $fruit) {
    echo "I like " . $fruit . "\n";
}

Output:

I like apple
I like banana
I like cherry

Each pass through the loop assigns the next element of $fruits to $fruit, in the same order the elements appear in the array, until there are no elements left.

Example 2: Iterating key => value pairs

<?php
$prices = [
    "Coffee" => 4.50,
    "Tea"    => 3.25,
    "Juice"  => 5.75,
];

foreach ($prices as $item => $price) {
    echo $item . ": $" . number_format($price, 2) . "\n";
}

Output:

Coffee: $4.50
Tea: $3.25
Juice: $5.75

Because $prices is an associative array, the $key => $value form is used to capture both the item name and its price on every iteration, letting the loop build a readable line for each entry.

Example 3: Nested arrays with destructuring

<?php
$students = [
    ["name" => "Maya", "scores" => [88, 92, 79]],
    ["name" => "Leo",  "scores" => [65, 70, 74]],
];

foreach ($students as ["name" => $name, "scores" => $scores]) {
    $average = array_sum($scores) / count($scores);
    echo $name . " average: " . round($average, 1) . "\n";
}

Output:

Maya average: 86.3
Leo average: 69.7

Here each element of $students is itself an associative array. Instead of assigning the whole sub-array to one variable and then indexing into it, the destructuring pattern ["name" => $name, "scores" => $scores] pulls the two fields straight into $name and $scores on every iteration, which keeps the loop body compact and readable.

How It Works, Step by Step

Let’s trace exactly what PHP does for the $prices example above:

  1. PHP evaluates $prices, confirms it is an array, and resets its internal pointer to the first entry, "Coffee" => 4.50.
  2. It checks whether a current element exists. It does, so PHP copies the key "Coffee" into $item and the value 4.50 into $price.
  3. The loop body runs: number_format() formats 4.50 as 4.50, and the line is echoed.
  4. PHP advances the internal pointer to the next entry, "Tea" => 3.25, and repeats the read-and-run cycle.
  5. This continues until the pointer moves past the last entry ("Juice"), at which point foreach exits and execution continues after the closing brace.

Because this was a by-value loop, none of this touched the original $prices array — reassigning $price inside the loop body would have had no lasting effect on it.

Common Mistakes

Mistake 1: Leaving a By-Reference Variable Dangling

This is the single most notorious foreach gotcha in PHP. When a by-reference loop finishes, the loop variable is still a reference to the last array element. If you reuse that variable name in a later, ordinary by-value foreach, you silently overwrite the last element of the array.

<?php
$array = [1, 2, 3, 4];

foreach ($array as &$value) {
    $value = $value * 2;
}
// $array is now [2, 4, 6, 8], but $value still references $array[3]

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

print_r($array);

Output:

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

The second loop’s $value is still bound to $array[3], so every assignment inside it (even though it looks like an ordinary by-value loop) writes into that last slot, and the original 8 is clobbered. Fix it by breaking the reference with unset() as soon as the by-reference loop ends:

<?php
$array = [1, 2, 3, 4];

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

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

print_r($array);

Output:

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

Mistake 2: Expecting a By-Value Loop to Modify the Array

Beginners often assume that changing the loop variable changes the array it came from. Without &, it never does, because $score only holds a copy of each value.

<?php
$scores = [10, 20, 30];

foreach ($scores as $score) {
    $score = $score + 5;
}

print_r($scores);

Output:

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

The array is completely unchanged, because every increment happened to a throwaway copy. To modify the array in place, iterate by reference and remember to unset() afterward:

<?php
$scores = [10, 20, 30];

foreach ($scores as &$score) {
    $score = $score + 5;
}
unset($score);

print_r($scores);

Output:

Array
(
    [0] => 15
    [1] => 25
    [2] => 35
)

Best Practices

  • Always call unset($value) immediately after a by-reference foreach loop to break the lingering reference.
  • Prefer a by-value loop unless you actually need to mutate the array — it is simpler and avoids the reference pitfall entirely.
  • Give $key and $value descriptive names ($userId => $user) rather than generic ones, especially in nested loops.
  • Use array destructuring (foreach ($rows as [$id, $name])) instead of manually indexing into each sub-array.
  • For very large or unknown-size datasets, iterate over a Generator instead of building a full array in memory first.
  • Never add or remove elements from the array you are iterating by key inside the loop body — build a new array or collect changes and apply them after the loop.
  • Check is_iterable($value) when a function accepts mixed input that will be passed to foreach, to fail predictably instead of triggering a warning.

Practice Exercises

  • Given $temps = [0, 20, 37, 100] (Celsius), use a foreach loop to print each value converted to Fahrenheit using the formula F = C * 9 / 5 + 32, one per line.
  • Given $inventory = ["Bolts" => 120, "Nails" => 3, "Screws" => 4, "Washers" => 50], loop through it with $item => $qty and, using continue, print only the items where $qty is less than 5, formatted as "Low stock: Nails (3)".
  • Given $words = [" hello ", "World ", " php"], write a by-reference foreach loop that trims whitespace from every element in place (don’t forget to unset() the reference), then print the resulting array with print_r().

Summary

  • foreach iterates arrays and any Iterator/IteratorAggregate/Generator object without manual index management.
  • Use foreach ($array as $value) for values only, or foreach ($array as $key => $value) to get keys too.
  • By-value iteration copies each element into the loop variable; by-reference iteration (&$value) binds directly to the array slot and can mutate the original array.
  • Always unset() a by-reference loop variable right after the loop to avoid the classic dangling-reference bug.
  • Array destructuring lets you unpack nested arrays or keyed sub-arrays directly in the as clause.
  • For objects, foreach transparently drives the Iterator protocol (rewind, valid, current, key, next).
  • Avoid adding or removing elements from an array while iterating over it by key — it leads to unpredictable results.