Unexpected behaviour in foreach while looping and unsetting in ArrayObject. An item is ignored
Asked Answered
C

2

10

(examples at the bottom!!!)

we have just upgrade our backend to PHP7 and after that, we have found a bug in our code related to an ArrayObject.

The code just loops over a copy of an Object (type native ArrayObject). The foreach iterates by value.

The purpose of the code is to filter some values that you don't need. In the example, if the iterated value is "two" or "three", unset it. I have tried it using iterator instead of the copied value and without the iterator.

Results:

- Iterator

  • PHP 5.6: works as expected, the returned value is the array without the values "two" and "three"
  • PHP 7: it only removes "two" and seems that the item with value "three" is not evaluated (see echo inside the loop, it doesn't print "three")

- No Iterator

  • PHP 5.6: gets a notice but works as expected, the returned value is the array without the values "two" and "three"
  • PHP 7: it only removes "two" and seems that the item with value "three" is not evaluated (see echo inside the loop, it doesn't print "three")

First loop => $key = 0, $value = "one" // continue

Second loop => $key = 1, $value = "second" // unset

Third loop => $key = 3, $value = "four" // WTF? where is the $key = 2, $value = "three"????

So I cannot understand what's going on. Our temporal solution is to iterate over the original object and unset from the copy. Does anybody knows which change in the PHP core (or ArrayObject/ArrayIterator) makes this? I have search about it but some people has this problem with foreach were the item iterated is by reference.

If you switch between PHP 5.6 and 7, the behaviour changes.

Example 1 (with iterator)

$elements = new ArrayObject();
$elements->append('one');
$elements->append('two');
$elements->append('three');
$elements->append('four');

print_r($elements);

$clone = clone $elements;
$it = $clone->getIterator();

echo "\n------\n";
foreach ($it as $key => $value) {
    echo $key."\t=>\t".$value."\n";
    if ($value == 'two' || $value == 'three') {
        $it->offsetUnset($key);
    }
}
echo "\n------\n";
print_r($clone);

Example 2 (without iterator)

$elements = new ArrayObject();
$elements->append('one');
$elements->append('two');
$elements->append('three');
$elements->append('four');

print_r($elements);

$clone = clone $elements;

echo "\n------\n";
foreach ($clone as $key => $value) {
    echo $key."\t=>\t".$value."\n";
    if ($value == 'two' || $value == 'three') {
        $clone->offsetUnset($key);
    }
}
echo "\n------\n";
print_r($clone);

Thanks so much!

Conde answered 17/10, 2016 at 13:17 Comment(6)
Please paste the actual code as text, not as a link or a picture.Cockayne
ArrayObject use iterator in both cases, and if you unset offset then possition in iterator is over one and skip next element. If you do $it->seek($key - 1); after $it->offsetUnset($key); then it should workSpirochaetosis
Yes @MarekJanoud, it could work but it doesn't explain any about the change between php 5.6 and php 7Conde
This is a known issue: bugs.php.net/bug.php?id=70246 Don't hold your horses on a fix, it's a tricky issue.Androgynous
Actually i'm not sure, and what i said $it->seek($key - 1); will not help, sorry, strange behaviour. Just follow answer and everything will be OKSpirochaetosis
fwiw, to delete keys by value, you could check this answerAphelion
B
1

From my understanding , it is considered a bad practice to modify an array while looping through it, and the proper way to do it would be using array_filter.

Since you have an ArrayObject, one solution would be to export it to an array, filter it using array_filter and create a new ArrayObject from the filtered array.

See also here : Filter ArrayObject (PHP)

Probably this behavior is due to the fact that loops are handled differently in php7. As mentioned here: http://php.net/manual/en/control-structures.foreach.php in php5 foreach uses an internal array pointer in contrast to php7.

Beliabelial answered 17/10, 2016 at 13:34 Comment(2)
@Beliabelial could be a bad practice and could be a good suggestion but....the main problem is that in php 5.6 it works as expected and in php 7 no. Anything has changed in the core that makes this. I think that may be anything about next() (of ArrayIterator)...Conde
The question is fully answered in this answer if you read the last paragraph followed by its link containing the full explanation. Pointers are used so the array is not directly modified. You'll have to revert to PHP 5.6 or rewrite the loop differently as this is the newly expected behavior.Jordon
A
0

You can give something like this a try:

$elements = new ArrayObject();
$elements->append('one');
$elements->append('two');
$elements->append('three');
$elements->append('four');

$iter = new ArrayIterator($elements->getArrayCopy());
foreach($iter as $content){
    if(in_array("one", $elements->getArrayCopy())) {
        $elements->offsetUnset($iter->key());
    }
}
    
print_r($elements);
Aluminize answered 26/2 at 18:23 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.