[php-src] Issue #22775: foreach by reference silently loses writes when the array expression has an inline (array) cast
[email protected] (w3K-one) Thu, 16 Jul 2026 19:45:54 +0000
| Newsgroups | php.bugs |
|---|---|
| Message-ID | <[email protected]> |
Issue: https://github.com/php/php-src/issues/22775
Author: 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:
```php
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
<?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
<?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
<?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).