Search code examples
phpreferencenull-coalescing-operator

PHP - foreach lose reference with null coalescing operator


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.


Solution

  • 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).