Skip to content

foreach by reference silently loses writes when the array expression has an inline (array) cast #22775

Description

@w3K-one

Description

When you iterate over an array with foreach by reference, and the array expression in the foreach statement itself has a redundant (array) cast applied to it, the writes made through the reference never make it back into the original array. No error, warning, or notice is raised. The loop body runs normally and the reference looks correct while you're inside the loop, but once the loop ends, the original variable is completely unchanged.

The array being cast is already an array, so the cast should be a no-op. Instead, writing the cast directly inline in the foreach expression causes PHP to iterate over some kind of temporary copy, and the by-reference binding attaches to that temporary instead of the real variable. Once the temporary goes out of scope at the end of the loop, every write made through the reference is lost.

If the same cast is done as its own statement on the line before the foreach, instead of being written inline in the foreach expression, the by-reference behavior works exactly as expected and nothing is lost. So this is specifically tied to writing the cast inline in the foreach() expression itself, not to using (array) in general.

I found this while debugging a real WordPress plugin. A function built an array of database rows and then did:

foreach ( (array) $rows as &$row ) {
    $row['art_url'] = some_function( $row['picture'] );
}
return $rows;

The (array) cast was defensive code, there in case $rows ever came back as something other than an array from the DB layer. $rows was always already a plain array in practice, so the cast should have done nothing. Instead every row silently failed to get the new field added to it, and there was no error anywhere pointing at why. It took a long diagnostic session to even notice the cast was the cause, since removing seemingly unrelated code fixed it.

I also confirmed this is not related to the well known "stale reference after foreach" gotcha where you forget to unset($row) after a loop and a later loop overwrites the last element. That requires a second foreach reusing the same reference variable name. My test has only one loop and no reused variable, and I tested both with and without unset($row) after the loop with the same result either way.

I reproduced the exact same script, unmodified, across three different machines, two different CPU architectures, and three unrelated PHP build sources, covering PHP 7.1 through 8.5, and every single combination reproduces this identically:

  1. x86_64 server, Debian trixie, CloudPanel managed PHP builds, versions 7.1 through 8.5, all reproduce.
  2. aarch64 (ARM64) server, Ubuntu jammy, CloudPanel managed PHP builds, versions 7.1 through 8.5, all reproduce.
  3. x86_64 server, Debian trixie, stock Debian distro package (PHP 8.4.23), reproduces.
  4. Official PHP Docker images (docker.io/library/php), versions 7.1 through 8.5, all reproduce.

3v4l.org link showing the same result grouped across PHP 8.2 through 8.5: https://3v4l.org/6TGo4

The following code:

<?php
$rows = [ [ 'x' => 1 ] ];
foreach ( (array) $rows as &$row ) {
    $row['y'] = 2;
}
echo json_encode( $rows ) . PHP_EOL;

Resulted in this output:

[{"x":1}]

But I expected this output instead:

[{"x":1,"y":2}]

For comparison, the same script with the cast removed:

<?php
$rows = [ [ 'x' => 1 ] ];
foreach ( $rows as &$row ) {
    $row['y'] = 2;
}
echo json_encode( $rows ) . PHP_EOL;

correctly prints [{"x":1,"y":2}].

And the same script with the cast moved to its own line before the foreach:

<?php
$rows = [ [ 'x' => 1 ] ];
$rows = (array) $rows;
foreach ( $rows as &$row ) {
    $row['y'] = 2;
}
echo json_encode( $rows ) . PHP_EOL;

also correctly prints [{"x":1,"y":2}].

PHP Version

Reproduced identically across every combination below:

x86_64, Debian trixie, CloudPanel builds: PHP 7.1.33-trixie, 7.2.34-trixie, 7.3.33-trixie, 7.4.33, 8.0.30, 8.1.34, 8.2.31, 8.3.31, 8.4.21, 8.5.6

aarch64, Ubuntu jammy, CloudPanel builds: PHP 7.1.33-jammy, 7.2.34-jammy, 7.3.33-jammy, 7.4.33, 8.0.30, 8.1.34, 8.2.31, 8.3.31, 8.4.21, 8.5.6

x86_64, Debian trixie, stock Debian package: PHP 8.4.23 (2:8.4+96)

Official PHP Docker images (docker.io/library/php): PHP 7.1.33, 7.2.34, 7.3.33, 7.4.33, 8.0.30, 8.1.34, 8.2.32, 8.3.32, 8.4.23, 8.5.8

Sample full version string (Furia, PHP 8.2):

PHP 8.2.31 (cli) (built: May 12 2026 07:08:08) (NTS)
Copyright (c) The PHP Group
Zend Engine v4.2.31, Copyright (c), by Zend Technologies
    with Zend OPcache v8.2.31, Copyright (c), by Zend Technologies

Operating System

Debian trixie and Ubuntu jammy, both 64-bit, tested on bare metal (x86_64 and aarch64) and inside Docker containers. CLI builds, non-thread-safe (NTS).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions