skip to Main Content

For me this is unexpected, but I’m sure someone can explain why in this block of code the final item to be printed is ‘four’, so the sequence would be:

onetwothreefourfiveonetwothreefourfour

The first sequence passes by reference, this is a broken down example of a function I am trying to debug and came across. I know how to fix the issue / refactor it but now curious why PHP works this way?

<?php

$items = ['one', 'two', 'three', 'four', 'five'];

foreach ($items as &$item) {
    echo $item;
}

foreach ($items as $item) {
    echo $item;
}

3

Answers


  1. You need to delete the reference before using the same variable name again.

    Demo: https://3v4l.org/qQBXP

    Also, there is no need for using a reference until you want to change the items.

    Read more about in the manual.

    Reason, why the last is four:

    When the second loop starts, it reuses the $item variable. But since it is a reference to the last element of the array, the assignment $item = 'five' changes the last element of the array to four, which is the last value $item had in the second loop.

    In other words: The pointer is still kept at the last element and it is overwritten with each iteration. In the 2nd loop the last will be overwritten with one. In the next iteration with two and so on.

    $items = ['one', 'two', 'three', 'four', 'five'];
    
    foreach ($items as &$item) {
        echo $item;
    }
    
    unset ($item);
    
    foreach ($items as $item) {
        echo $item;
    }
    
    Login or Signup to reply.
  2. anyber‘s demo at https://3v4l.org/b2eIb (from the comments) is very helpful.

    I will reproduce the code and output here, for ease:

    Code

    <?php
    
    $items = ['one', 'two', 'three', 'four', 'five'];
    
    foreach ($items as $key => &$item) {
        echo "$key - $itemn";
        
        var_dump($items);
    }
    echo "nrestartn";
    foreach ($items as $key => $item) {
        echo "$key - $itemn";
        var_dump($items);
    }
    

    Output

    0 - one
    array(5) {
      [0]=>
      &string(3) "one"
      [1]=>
      string(3) "two"
      [2]=>
      string(5) "three"
      [3]=>
      string(4) "four"
      [4]=>
      string(4) "five"
    }
    1 - two
    array(5) {
      [0]=>
      string(3) "one"
      [1]=>
      &string(3) "two"
      [2]=>
      string(5) "three"
      [3]=>
      string(4) "four"
      [4]=>
      string(4) "five"
    }
    2 - three
    array(5) {
      [0]=>
      string(3) "one"
      [1]=>
      string(3) "two"
      [2]=>
      &string(5) "three"
      [3]=>
      string(4) "four"
      [4]=>
      string(4) "five"
    }
    3 - four
    array(5) {
      [0]=>
      string(3) "one"
      [1]=>
      string(3) "two"
      [2]=>
      string(5) "three"
      [3]=>
      &string(4) "four"
      [4]=>
      string(4) "five"
    }
    4 - five
    array(5) {
      [0]=>
      string(3) "one"
      [1]=>
      string(3) "two"
      [2]=>
      string(5) "three"
      [3]=>
      string(4) "four"
      [4]=>
      &string(4) "five"
    }
    
    restart
    0 - one
    array(5) {
      [0]=>
      string(3) "one"
      [1]=>
      string(3) "two"
      [2]=>
      string(5) "three"
      [3]=>
      string(4) "four"
      [4]=>
      &string(3) "one"
    }
    1 - two
    array(5) {
      [0]=>
      string(3) "one"
      [1]=>
      string(3) "two"
      [2]=>
      string(5) "three"
      [3]=>
      string(4) "four"
      [4]=>
      &string(3) "two"
    }
    2 - three
    array(5) {
      [0]=>
      string(3) "one"
      [1]=>
      string(3) "two"
      [2]=>
      string(5) "three"
      [3]=>
      string(4) "four"
      [4]=>
      &string(5) "three"
    }
    3 - four
    array(5) {
      [0]=>
      string(3) "one"
      [1]=>
      string(3) "two"
      [2]=>
      string(5) "three"
      [3]=>
      string(4) "four"
      [4]=>
      &string(4) "four"
    }
    4 - four
    array(5) {
      [0]=>
      string(3) "one"
      [1]=>
      string(3) "two"
      [2]=>
      string(5) "three"
      [3]=>
      string(4) "four"
      [4]=>
      &string(4) "four"
    }
    

    Explanation

    Each time the loop runs, $item is assigned a value from the array, but because it’s passed by reference, it’s linked to that entry in the array (hence the &s in the dumped output).

    The next run of the loop reassigns the reference to the next array entry. This all works fine for the first loop and doesn’t interefere with anything.

    But notice at the end of the first loop, the last entry in the array still retains its reference to $item. Then when you start looping again, we can observe that the last entry in the array is, each time, changed into the value of the current $item value.

    In the second loop, the reference no longer passes from entry to entry this time, but remains on the last entry in the array. This is because in the second loop $item is not being passed by reference – that’s the important difference.

    Hence by the time you get to the last iteration of the second loop, the 5th entry has the value "four", and since that’s also the current index of the loop, there’s nothing else to change it to.


    Resolution

    If we were to pass $item by reference in both loops (https://3v4l.org/DgYQ0) or neither (https://3v4l.org/cXnIR) we would get the output you were expecting, with five as the last output value.

    A third way to resolve it would be to leave both loops as they are, but unset the $item value between each one (https://3v4l.org/5Xa7I) as suggested in this answer.

    The last and perhaps most obvious solution is simply to use a different variable name in the second loop (https://3v4l.org/JjiEu) – which is always a good principle anyway in a language like PHP, where loop variables stay in scope after the loop ends.

    Login or Signup to reply.
  3. This behaviour is easiest to understand if you "unroll" the two foreach loops, replacing each iteration of the as &$item with assign-by-reference, and each iteration of the as $item as normal assignment:

    $items = ['one', 'two', 'three', 'four', 'five'];
    
    foreach ($items as &$item) {
        echo $item;
    }
    
    foreach ($items as $item) {
        echo $item;
    }
    

    Becomes something like this:

    $items = ['one', 'two', 'three', 'four', 'five'];
    
    $item =& $items[0];
    echo $item;
    $item =& $items[1];
    echo $item;
    $item =& $items[2];
    echo $item;
    $item =& $items[3];
    echo $item;
    $item =& $items[4];
    echo $item;
    
    $item = $items[0];
    echo $item;
    $item = $items[1];
    echo $item;
    $item = $items[2];
    echo $item;
    $item = $items[3];
    echo $item;
    $item = $items[4];
    echo $item;
    

    The key thing to notice is the last assignment from the first loop, and the first assignment in the next:

    $item =& $items[4];
    $item = $items[0];
    

    This does an assignment-by-reference, binding $item and $items[4] together; then, we assign by value to $item, which is equivalent to assigning to $items[4]. In other words, we’re doing this:

    $items[4] = $items[0];
    

    Since we never break the link to $items[4], every assignment to $items continues to point there, as though we had this:

    $items[4] = $items[0];
    echo $items[4];
    $items[4]= $items[1];
    echo $items[4];
    $items[4]= $items[2];
    echo $items[4];
    $items[4]= $items[3];
    echo $items[4];
    $items[4]= $items[4];
    echo $items[4];
    

    The solution meanwhile, is to have a very simple rule: whenever you foreach by reference, call unset afterwards:

    $items = ['one', 'two', 'three', 'four', 'five'];
    
    foreach ($items as &$item) {
        echo $item;
    }
    unset($item); // break reference
    
    foreach ($items as $item) {
        echo $item;
    }
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search