PHP – foreach lose reference with null coalescing operator-ThrowExceptions

Exception or error:

Q1:
I think the ?? will do nothing when:

$a = [1, 2];
foreach ($a ?? [] as &$v) {
    $v++;
}
var_dump($a);

But why?

array(2) {
  [0]=>
  int(1)
  [1]=>
  int(2)
}

Q2:
This is more strange:

foreach ($a = [1, 2] as &$v) {
    $v++;
}
var_dump($a);
// output
array(2) {
  [0]=>
  int(1)
  [1]=>
  int(2)
}

My thinking:
I think the expressions are not referencable, but foreach catch the error or somehow and then make a copy.
References that work:

$a = 1;
$c = &$a;

Do not work:

$a = 1;
$c = &($a);
$c = &($a ?? []);
$c = &($a + 1);

Dos ?? make a copy? I just don’t want to wrap the foreach with a if (isset($a)) if $a is null and foreach will fail.

How to solve:

TL;DR For your case, you could consider using the null coalesce operator in this manner:

$a = $a ?? [];
foreach ($a as &$v) { ... }

Or, don’t use references at all, by either using array_map() or by using the keys to make modifications in the underlying array.

Q1

$a = [1, 2];
foreach ($a ?? [] as &$v) {
    $v++;
}
var_dump($a);

The coalesce operator uses a copy of the original array, and then applies the right hand operand if null. Therefore, the iteration happens over a copy of the original array.

You could compare this to the following:

$a = [1, 2];
$x = $a ?? [];
$x[1] = 4;
var_dump($a); // [1, 2]

Code Insight

compiled vars:  !0 = $a, !1 = $v
line     #* E I O op                           fetch          ext  return  operands
-------------------------------------------------------------------------------------
   8     0  E >   ASSIGN                                                   !0, <array>
   9     1        COALESCE                                         ~3      !0
         2        QM_ASSIGN                                        ~3      <array>
         3      > FE_RESET_RW                                      $4      ~3, ->8
... rest of looping code

The first operand of FE_RESET_RW is the hash variable that will be iterated over, and you can see that it’s ~3 instead of !0 ($a in your code), which is what you expected to happen.

Q2

foreach ($a = [1, 2] as &$v) {
    $v++;
}

What happens here is that the return value of the assignment $a = [1, 2] gets used as the array to iterate over.

You can compare this behaviour to something like this:

$x = $a = [1, 2];
$x[0] = 4; // modify in-place
var_dump($a); // [1, 2]

Code Insight

compiled vars:  !0 = $a, !1 = $v
line     #* E I O op                           fetch          ext  return  operands
-------------------------------------------------------------------------------------
   3     0  E >   ASSIGN                                           $2      !0, <array>
         1      > FE_RESET_RW                                      $3      $2, ->6
... rest of looping code

Again, $2 is the first operand of FE_RESET_RW, which is the assignment result, and so iteration will not happen against !0 ($a in your code).

Answer:

You can use the expanded array syntax to get the index, and then use that to dereference the original array value:

$a = [1, 2];
foreach ($a ?? [] as $i => $v) {
    ++$a[$i];
}
var_dump($a);

But note this is likely useless anyway, because if $a isn’t set (so that the ?? qualifies) then the loop will make zero iterations and $a will still be unset for the var_dump(). (Unless that’s what you need, I suppose…)

Answer:

I just dont want to wrap the foreach with a if (isset($a)) if $a is null and foreach will fail.

This is unavoidable if you haven’t initialized variables to the correct type, but here’s some trickery in a utility function using pass by reference, return by reference and a default value:

function &test(&$array=[]) {
    return $array;
}

$a = [1, 2];

foreach (test($a) as &$v) {
    $v++;
}

Doesn’t generate errors if $a is not set and doesn’t loop, however in the above it yields:

array(2) {
  [0]=>
  int(2)
  [1]=>
  &int(3)
}

In PHP arrays are assigned by value (assignment copies), so that if $a !== null then $a ?? [] returns the value of $a or [1, 2]. So $a is not modified by the reference to the values of this value using &$v.

Objects are assigned by reference so a reference is returned in this case and the original object is modified, unless $a is not set. Then you will obviously get:

Notice: Undefined variable: a

$a = (object)[1, 2];

foreach ($a ?? [] as &$v) {
    $v++;
}
var_dump($a);

This yields:

object(stdClass)#1 (2) {
  ["0"]=>
  int(2)
  ["1"]=>
  &int(3)
}

From Assignment by Reference:

Assignment by reference is also supported, using the "$var = &$othervar;” syntax. Assignment by reference means that both variables end up pointing at the same data, and nothing is copied anywhere.

In these cases, they are all expressions and cannot be referenced:

$c = &($a);
$c = &($a ?? []);
$c = &($a + 1);

If this is just an exercise that’s fine, but if you are trying to solve a specific problem then you need to outline that broader problem.

Leave a Reply

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