Reputation: 1087
<? foreach ($this->criteria as $key => $value): ?>
<li><?= $this->accommodationsLink($this->criteria, $key) ?></li>
<? endforeach ?>
This code give unexpected results, because only one link is visible. But there are two items in $this->criteria.
I explored the cause of the probleem. In the function accommodationsLink is another foreach loop that works on 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 a simple way to let this code work or are nested foreach loops not possible with php iterator interface?
Upvotes: 6
Views: 2995
Reputation: 27190
I tried this with both plain arrays and PHP iterators. Unfortunately PHP iterators, since they're objects, work differently. Objects are passed by reference while arrays are by-value. So when the nested foreach reaches the end of the iterator the first foreach can't resume where it stopped because the internal pointer is set to the last element.
Consider the following example written using a plain 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 produces the following output:
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 the same thing with an iterator we get quite a different result. To avoid confusion I will use the ArrayIterator class so that everything is already implemented by the PHP guys and we don't end up using interfaces the wrong way. So no room for errors here, this is how 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";
}
}
Output is:
first loop: 0
second loop: 0
second loop: 1
second loop: 2
As you can see the first foreach gets executed only once.
A workaround could be implementing the SeekableIterator interface. It would let us use the seek() method to reset the internal pointer to its correct value. It's a bad practice in my opinion but if the PHP guys don't fix this thing I can't really say what it could be best. I'd probably avoid iterators from now on since they seem to behave differently from arrays which I think it's what people assume at first. So using them would make my application error prone because it could be that a dev in my team doesn't know about this and messes with the code.
Follows an example with 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);
}
Output is as anyone 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 array copy. Iterators, since they are objects, are passed by reference. Therefore each foreach shares the same object. The same thing happens if you try to unset an element in the nested foreach. The unset will increment the internal pointer. Then the execution reaches the end of the nested foreach and the internal pointer gets increased again. This means that with an unset we increase the internal pointer two times. The parent foreach will therefore skip an element.
My advice is, if you can't avoid iterators, be REALLY REALLY careful. Always unit test them thoroughly.
Note: Code tested on PHP 5.6.14 and PHP 7.0.0 RC5.
Upvotes: 2
Reputation: 3695
EDIT:
after posting this I realised this will break badly if you do continue
or break
in the nested foreach. So this is probably not your desired solution.
As stated in other answers PHP foreach calls rewind
at the beginning of the foreach
loop and valid
at the end of each iteration. So in the nested foreach
iterator gets invalid and stays this way in parent foreach
. Here is a hackish workaround that uses 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);
}
}
output:
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"
Upvotes: 1
Reputation: 165271
Well, the second foreach is going to call $iterator->reset()
prior to running. So when the second foreach reaches the end of the iterator, the internal pointer is already at the end of the array...
It'd be like:
$it->reset();
while ($it->valid()) {
$it->reset();
while ($it->valid()) {
//do something
$it->next();
}
$it->next();
}
Buy the time it gets to the $it->next()
call in the outer loop, it's already invalid. So the next()
call will "fail", and $it->valid()
would return false.
It's not a problem with iterators, it's a problem with the logic you're using. If you really must nest loops, then clone
the iterator ($subit = clone $it
) in the inner loop so you don't disturb 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
}
}
Upvotes: 2