2

I'm experiencing unusual behavior in PHP while iterating over arrays, and would love either an explanation or a confirmation that this is a bug.

For starters, here's the PHP version:

PHP 7.2.3 (cli) (built: Mar  8 2018 10:30:06) ( NTS )
Copyright (c) 1997-2018 The PHP Group
Zend Engine v3.2.0, Copyright (c) 1998-2018 Zend Technologies
    with Zend OPcache v7.2.3, Copyright (c) 1999-2018, by Zend Technologies

Here's the sample script I created to replicate the behavior:

<?php
$example = [
    [
        'a' => 'A',
        'b' => 'B',
    ],
    [
        'c' => 'C',
        'd' => 'D',
    ],
    [
        'e' => 'E',
        'f' => 'F'
    ]
];

foreach ($example as &$letters) {
    foreach ($letters as &$letter) {
        // Do nothing
    }
}

print_r($example);

foreach ($example as $letters) {
    print_r($letters);
}
?>

And here's the output:

Array
(
    [0] => Array
        (
            [a] => A
            [b] => B
        )

    [1] => Array
        (
            [c] => C
            [d] => D
        )

    [2] => Array
        (
            [e] => E
            [f] => F
        )

)
Array
(
    [a] => A
    [b] => B
)
Array
(
    [c] => C
    [d] => D
)
Array
(
    [c] => C
    [d] => D
)

The unexpected behavior is that the second-to-last element replaces the last element only during the second iteration over the array. This is not seen, though, when printing the whole array.

This only happens if there's a nested foreach loop where the values are used by reference. The length of the array does not seem to affect the behavior; the last element is always replaced with the second-to-last element.

Additionally, it appears that the problem is resolved if an unset($letters); statement is added before the last foreach loop.

Is this intentional, or a bug with PHP?

EDIT: See duplicate question above, and specifically this article, which demonstrates visually why the behavior is the way that it is.

Wes Cossick
  • 2,923
  • 2
  • 20
  • 35
  • 1
    You have to unset the reference to avoid that behavior. It's in the documentation for foreach if I remember correctly. – Don't Panic Mar 09 '18 at 21:10
  • Your `foreach ($letters as &$letter)` line affects the structure of the array because you have the ampersands. I'm not sure exactly where the change is happening but if you remove those ampersands, you should get more predictable behavior. – S. Imp Mar 09 '18 at 21:11
  • 1
    http://php.net/manual/en/control-structures.foreach.php check the pink **warning** section. – Don't Panic Mar 09 '18 at 21:12
  • 1
    The answer to [this question](https://stackoverflow.com/questions/5810168/php-foreach-by-reference-causes-weird-glitch-when-going-through-array-of-objects) has a link to an article that illustrates the behavior. It isn't considered a bug by PHP, though. – Don't Panic Mar 09 '18 at 21:16
  • 1
    Read about PHP [references](http://php.net/manual/en/language.references.whatdo.php#language.references.whatdo.assign). – axiac Mar 09 '18 at 21:20
  • @Don'tPanic, the article you mentioned is what I was looking for. I was not looking to fix this code; rather, I was looking to understand why this was happening. If you add an answer with the link to that article, I'll accept it as the correct one. – Wes Cossick Mar 09 '18 at 21:22
  • 2
    I'll add it to the duplicate list here. No need for me to reinvent the wheel. :) And you know, I said "it isn't considered a bug by PHP", but it really just isn't a bug. It's just a non-intuitive consequence of using references like that. – Don't Panic Mar 09 '18 at 21:23
  • @Don'tPanic, sweet, good call! And yeah, agreed that it's non-intuitive, but I certainly see how it's not a bug. – Wes Cossick Mar 09 '18 at 21:25

2 Answers2

1

Because you're leaving references just lying around waiting to break things. You need to unset() them after the loop to avoid this.

foreach ($example as &$letters) {
    foreach ($letters as &$letter) {
        // Do nothing
    }
    unset($letter);
}
unset($letters);

Also, you should use var_dump() instead of print_r() as it shows you far more useful information that gives you clues to this and many other problems.

eg: Your above, broken output would look like:

array(3) {
  [0]=>
  array(2) {
    ["a"]=>
    string(1) "A"
    ["b"]=>
    string(1) "B"
  }
  [1]=>
  array(2) {
    ["c"]=>
    string(1) "C"
    ["d"]=>
    string(1) "D"
  }
  [2]=>
  &array(2) {
    ["e"]=>
    string(1) "E"
    ["f"]=>
    &string(1) "F"
  }
}
array(2) {
  ["a"]=>
  string(1) "A"
  ["b"]=>
  string(1) "B"
}
array(2) {
  ["c"]=>
  string(1) "C"
  ["d"]=>
  string(1) "D"
}
array(2) {
  ["c"]=>
  string(1) "C"
  ["d"]=>
  string(1) "D"
}

Which shows that the last element in the array and sub-array are still references which causes problems in the subsequent loop.

Sammitch
  • 30,782
  • 7
  • 50
  • 77
0

I believe foreach ($example as &$letters) sets up a variable $letters that points to the original array for the purpose of altering it (rather than making a copy for iteration which is default behavior) and your next statement foreach ($letters as &$letter) modifies the array because it modifies $letters.

Remove the ampersands as I have here:

$example = [
    [
    'a' => 'A',
    'b' => 'B',
    ],
    [
    'c' => 'C',
    'd' => 'D',
    ],
    [
    'e' => 'E',
    'f' => 'F'
    ]
];

foreach ($example as $letters) {
    foreach ($letters as $letter) {
    // Do nothing
    }
}

print_r($example);

foreach ($example as $letters) {
    print_r($letters);
}

And this will be your output:

Array
(
    [0] => Array
    (
        [a] => A
        [b] => B
    )

    [1] => Array
    (
        [c] => C
        [d] => D
    )

    [2] => Array
    (
        [e] => E
        [f] => F
    )

)
Array
(
    [a] => A
    [b] => B
)
Array
(
    [c] => C
    [d] => D
)
Array
(
    [e] => E
    [f] => F
)

Note that if you absolutely must use the ampersands because you do actually want to make changes to the original array, just removing one ampersand will produce that same output:

foreach ($example as $letters) { // removed the ampersand here
    foreach ($letters as &$letter) {
        // Do nothing
    }
}
S. Imp
  • 2,833
  • 11
  • 24
  • Sorry, not looking to change/fix the initial loops, but rather for an explanation regarding why the behavior is the way that it is when dealing with references. – Wes Cossick Mar 09 '18 at 21:24
  • @WesCossick it sounds like you don't understand what [assigning a variable by reference](http://php.net/manual/en/language.references.php) does. It is your use of the ampersands (which cause vars to be assigned by reference) that results in the original array being altered. Your code is careless. – S. Imp Mar 10 '18 at 00:45
  • I think you're assuming that this question was seeking to fix some form of broken code. That is not the case. I didn't just 'accidentally' write a nested `foreach` loop using references that does nothing at all. It was constructed purposefully for this question to create a particular behavior. The question was seeking an explanation for why that behavior occurred, not how to side-step the behavior entirely. – Wes Cossick Mar 12 '18 at 19:15