Nested foreach with iterator interface

<? foreach ($this->criteria as $key => $value): ?> <li><?= $this->accommodationsLink($this->criteria, $key) ?></li> <? endforeach ?> 

This code gives unexpected results because only one link is visible. But there are two elements to the $ this-> criteria.

I investigated the cause of the problem. In the accommodationLink function, another foreach loop that works with the same criteria object

 foreach ($criteria as $key => $value) { $params[$key] = $value; } 

$ this-> criteria and $ criteria are the same object that implements the php Iterator interface. Is there an easy way to let this code work or are nested foreach loops not possible using the php iterator interface?

+4
source share
3 answers

Well, the second foreach is going to call $iterator->reset() before starting. Therefore, when the second foreach reaches the end of the iterator, the internal pointer is already at the end of the array ...

It will look like this:

 $it->reset(); while ($it->valid()) { $it->reset(); while ($it->valid()) { //do something $it->next(); } $it->next(); } 

Buy the time received during the call of $it->next() in the outer loop, it is no longer valid. Thus, calling next() will fail, and $it->valid() will return false.

This is not a problem with iterators, this is a problem with the logic you use. If you really need loops then clone iterator ( $subit = clone $it ) in the inner loop so you don't interfere with the pointer ...

Edit: Example with cloning:

 $it->reset(); while ($it->valid()) { $bar = clone $it; $bar->reset(); while ($bar->valid()) { //do something $bar->next(); } $it->next(); } 

Or using foreach (which is semantically equivalent):

 foreach ($it as $key => $value) { $subit = clone $it; foreach ($subit as $k => $v) { //Do stuff } } 
+2
source

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.

+1
source

EDIT: after posting this, I realized that this will break a lot if you do continue or break in a nested foreach. So this is probably not your desired solution.

As pointed out in other answers, PHP foreach calls rewind at the beginning of the foreach and valid foreach at the end of each iteration. Thus, in the nested foreach iterator becomes invalid and thus remains in the parent foreach . Here's a hacky workaround that uses a stack of pointers instead of a single pointer and makes this iterator behave like arrays in this case.

 class Test implements Iterator { private $loopstack = []; private $array = array("A", "B", "C",); function rewind() { $this->loopstack[] = 0; } function current() { return $this->array[end($this->loopstack)]; } function key() { return end($this->loopstack); } function next() { array_push($this->loopstack, array_pop($this->loopstack) + 1); } function valid() { $valid = isset($this->array[end($this->loopstack)]); if (!$valid) { array_pop($this->loopstack); } return $valid; } } $iterator = new Test(); foreach ($iterator as $e){ var_dump('loop1 ' . $e); foreach ($iterator as $e2){ var_dump('loop2 ' . $e2); } } 

exit:

 string(7) "loop1 A" string(7) "loop2 A" string(7) "loop2 B" string(7) "loop2 C" string(7) "loop1 B" string(7) "loop2 A" string(7) "loop2 B" string(7) "loop2 C" string(7) "loop1 C" string(7) "loop2 A" string(7) "loop2 B" string(7) "loop2 C" 
0
source

All Articles