Refactoring PHP Code: Transitioning from each() to foreach() Loops

Refactoring PHP Code: Transitioning from each() to foreach() Loops
Refactoring PHP Code: Transitioning from each() to foreach() Loops

Learn how to refactor PHP legacy code by replacing deprecated each() loops with more efficient foreach() loops. This guide provides step-by-step examples to modernize your code.

Sooner or later, you inherit a PHP codebase that still leans on each(). You bump the server to PHP 8, hit the site, and get a fatal error, because each() was deprecated in PHP 7.2 and removed outright in PHP 8.0. There’s no flag to turn it back on. The fix is to move those loops to foreach(), and the good news is that it usually makes the code shorter and easier to read.

We’ll walk through how each() was typically used, from the simple case to nested loops, and rewrite each one with foreach(). We’ll also flag the couple of spots where the swap isn’t a straight one-to-one, so you don’t trade a fatal error for a silent bug.

What Was the each() Function?

each() returned the current key-value pair from an array and moved the array’s internal pointer forward one step. Each call handed you a four-element array: index 0 and key key both held the key, index 1 and key value both held the value. You’d usually pair it with a while loop to walk the whole array.

Example: Using each() with while
PHP
<?php
$array = ['name' => 'Alice', 'age' => 30, 'email' => '[email protected]'];
reset( $array ); // Reset pointer to the beginning
while ( list( $key, $value ) = each( $array ) ) {
    echo "$key: $value\n";
}

Here each() feeds a while loop, and list() pulls the key and value out of that four-element return. It worked, but it relies on the array pointer and a manual reset(), and that’s exactly the kind of state that trips people up. foreach() does the same job without any of it.

Why Migrate to foreach()?

foreach() doesn’t touch the internal array pointer, so you don’t have to think about reset() or where the pointer happens to be. It’s shorter, it reads plainly, and it’s the replacement the PHP manual itself points you to. Since each() is gone in PHP 8, this isn’t really optional anymore. If you want your code to run on a current PHP, the loops have to move.

Refactoring each() to foreach()

Let’s take that first example and rewrite it. Same output, less machinery.

Example: Refactoring each() with foreach()
PHP
<?php
$array = ['name' => 'Alice', 'age' => 30, 'email' => '[email protected]'];
foreach ( $array as $key => $value ) {
    echo "$key: $value\n";
}

foreach() handles the walk for you. No reset(), no pointer, no list(). That’s the whole win in one small block, and it’s the same shape you’ll apply everywhere else.

Handling Complex each() Scenarios

Real legacy code is rarely one clean loop. You’ll find nested each() calls and logic keyed off the array. Here’s a common one: an array of users, each with its own set of attributes.

Example: Nested each() Loops

The old version resets the pointer on both the outer and inner arrays:

PHP
<?php
$users = [
    'user1' => ['name' => 'Alice', 'age' => 30],
    'user2' => ['name' => 'Bob', 'age' => 25]
];
reset( $users ); // Reset pointer for outer loop
while ( list( $userKey, $attributes ) = each( $users ) ) {
    echo "$userKey:\n";
    reset( $attributes ); // Reset pointer for inner loop
    while ( list( $attrKey, $attrValue ) = each( $attributes ) ) {
        echo "  $attrKey: $attrValue\n";
    }
}

Two loops, two pointers, two resets to keep straight. Here it is with foreach() on both levels:

Refactored Example: Using foreach() for Nested Loops
PHP
<?php
$users = [
    'user1' => ['name' => 'Alice', 'age' => 30],
    'user2' => ['name' => 'Bob', 'age' => 25]
];
foreach ( $users as $userKey => $attributes ) {
    echo "$userKey:\n";
    foreach ( $attributes as $attrKey => $attrValue ) {
        echo "  $attrKey: $attrValue\n";
    }
}

Same result, and each loop stands on its own. There’s nothing shared between the outer and inner walk, so there’s nothing to reset and nothing to get out of sync.

Best Practices When Refactoring each() to foreach()

A few things worth keeping in mind as you go through old code:

  • Simplify as you go: a lot of each() plus while plus reset() scaffolding collapses into a single foreach() line. Take the reduction.
  • Test thoroughly: the swap looks mechanical, but nested and multi-level arrays are where the odd assumption hides. Run the code paths you’re touching.
  • Keep your keys: foreach ( $array as $key => $value ) gives you both the key and the value, so anything that read the key from each() still has it.
  • Leave it readable: you’re already in there. Name the variables clearly so the next person doesn’t have to reverse-engineer the loop.
Edge Cases When Refactoring each()

Most swaps are clean, but two situations need a second look.

Code that leans on the array pointer

If the old logic depends on where the array pointer sits, on reset(), next(), prev(), or current() being interleaved with the loop, foreach() won’t preserve that behavior. It always starts at the beginning and manages its own position. Read that code carefully before you cut it over, because the fatal error is obvious but a shifted pointer is not.

The reference pitfall

By default foreach() works on a copy of each value, so assigning to the loop variable doesn’t change the array. If you deliberately want to modify the array in place, you use a reference with &. That’s fine, but it has a well-known trap: the reference to the last element survives after the loop ends, and the next loop that reuses that variable name can quietly overwrite it. Always unset() the reference the moment the loop is done.

PHP
<?php
$arr = [ 1, 2, 3, 4 ];
foreach ( $arr as &$value ) {
    $value = $value * 2;
}
unset( $value ); // break the reference before it bites you
Performance Considerations

The PHP manual lists foreach() as faster than the old each() approach, on top of being easier to read. For most code the difference is not why you’re switching, correctness on PHP 8 is, but on very large arrays skipping the manual pointer work does add up.

Benchmarking each() vs foreach()

One honest caveat before you run anything like the snippet below: since each() was removed in PHP 8.0, this benchmark only runs on PHP 7.x. On PHP 8 the each() half throws a fatal error. It’s here to show the historical comparison, not as something to drop into current code.

PHP
<?php
$array = array_fill( 0, 10000, 'value' );
// Benchmark each()
$start = microtime( true );
reset( $array );
while ( list( $key, $value ) = each( $array ) ) {
    // do nothing
}
$end = microtime( true );
echo 'each(): ' . ($end – $start) . ' seconds';
// Benchmark foreach()
$start = microtime( true );
foreach ( $array as $key => $value ) {
    // do nothing
}
$end = microtime( true );
echo 'foreach(): ' . ($end – $start) . ' seconds';

On PHP 7 you’ll see foreach() come out ahead, especially as the array grows. On PHP 8 the point is moot, each() simply isn’t there to race.

Conclusion

Moving from each() to foreach() isn’t a nice-to-have refactor, it’s what keeps the code running once you’re on PHP 8. In almost every case the new loop is shorter and clearer. Just watch the two edge cases, code that leans on the array pointer and references you forget to unset(), and the migration is about as painless as legacy work gets.

Leave a Comment

Your email address will not be published. Required fields are marked *


Scroll to Top