I tried this with equal arrays as well as with PHP iterators. Unfortunately, PHP iterators, since they are objects, work differently. Objects are passed by reference, and arrays are passed by value. Therefore, when a nested foreach reaches the end of the iterator, the first foreach cannot resume where it stopped, because the internal pointer is set to the last element.
Consider the following example written using a simple PHP array:
$test = [1, 2, 3]; foreach ($test as $i1 => $v1) { echo "first loop: $i1\n"; foreach ($test as $i2 => $v2) { echo "second loop: $i2\n"; } }
The above snippet outputs the following result:
first loop: 0 second loop: 0 second loop: 1 second loop: 2 first loop: 1 second loop: 0 second loop: 1 second loop: 2 first loop: 2 second loop: 0 second loop: 1 second loop: 2
If we try to do the same with the iterator, we get a completely different result. To avoid confusion, I will use the ArrayIterator class, so everything is already implemented by the PHP guys, and we are not using the wrong path interfaces. There is no room for errors, since iterators are implemented by them:
$test = new ArrayIterator([1, 2, 3]); foreach ($test as $i1 => $v1) { echo "first loop: $i1\n"; foreach ($test as $i2 => $v2) { echo "second loop: $i2\n"; } }
Exit:
first loop: 0 second loop: 0 second loop: 1 second loop: 2
As you can see, the first foreach is launched only once.
A workaround could be the implementation of the SeekableIterator interface. This would allow us to use the seek () method to reset the internal pointer to its correct value. In my opinion, this is bad practice, but if the guys from PHP do not fix this thing, I canβt say that it could be better. I would probably avoid iterators since then, since they seem to behave differently than arrays, which I think people first assume. Thus, using them can lead to the tendency of my application to error, because it may be that the developer in my team does not know about this and does not work with the code.
Follow the example of the SeekableIterator interface:
class MyIterator implements SeekableIterator { private $position = 0; private $array = [1, 2, 3]; public function __construct() { $this->position = 0; } public function rewind() { $this->position = 0; } public function current() { return $this->array[$this->position]; } public function key() { return $this->position; } public function next() { ++$this->position; } public function valid() { return isset($this->array[$this->position]); } public function seek($position) { $this->position = $position; } } $test = new MyIterator(); foreach ($test as $i1 => $v1) { echo "first loop $i1\n"; foreach ($test as $i2 => $v2) { echo "second loop $i2\n"; } $test->seek($i1); }
The conclusion will be as one would expect:
first loop: 0 second loop: 0 second loop: 1 second loop: 2 first loop: 1 second loop: 0 second loop: 1 second loop: 2 first loop: 2 second loop: 0 second loop: 1 second loop: 2
All this happens because each foreach works on its own copy of the array. Iterators, because they are objects, are passed by reference. Therefore, each foreach has the same object. The same thing happens if you try to disable an element in a nested foreach. Unset will increment the internal pointer. Then, execution reaches the end of the nested foreach, and the internal pointer increases again. This means that with unset, we double the internal pointer. Therefore, the parent foreach skips the element.
My advice is, if you cannot avoid iterators, REALLY REALLY careful. Always unit test them carefully.
Note: Code tested on PHP 5.6.14 and PHP 7.0.0 RC5.