hakre
hakre

Reputation: 198119

When does the NoRewindIterator rewind the inner Iterator?

By its name it is perfectly clear when PHP's NoRewindIterator rewinds: never.

But sometimes, esp. when extending from it, I ask myself: Does it ever rewind? Does it rewind once? So if I need it, do I need to implement the (first) rewind as some Traversable might require it?

I specifically wonder about the inner iterator here, so the Iterator the NoRewindIterator class takes as parameter with its constructor.

For example:

$iterator = new SomeIterator(); // implements Iterator
$noRewind = new NoRewindIterator($iterator);
foreach ($noRewind as $value) {
    break;
}

Will $iterator->rewind() be never called when using $noRewind from here on? Or will it be called once for the first time $noRewind->rewind() is invoked (e.g. within the first foreach in the example). Or maybe on construct?

Upvotes: 1

Views: 759

Answers (2)

sanmai
sanmai

Reputation: 30911

NoRewindIterator can sort of rewind in some cases.

E.g. when you use continue to a cycle outside:

<?php
$iterator = new NoRewindIterator(new ArrayIterator([1, 2, 3, 4]));
foreach ([1, 2, 3, 4] as $a) {
    foreach ($iterator as $b) {
        echo "$a\t$b\n";
        continue 2;
    }
}

One could expect it to print ones be ones, twos by twos, and so on, but actually this code prints:

1   1
2   1
3   1
4   1

Swapping continue 2 with a plain break does not change a thing.

<?php
$iterator = new \NoRewindIterator(new \ArrayIterator([1, 2, 3, 4]));

$array = [1, 2, 3, 4];

foreach ($array as $a) {
    foreach ($iterator as $b) {
        echo "$a\t$b\n";
        break;
    }
}

If you wrap this iterator with a debugging iterator you would see that next() never gets called if you break the loop by any means. Therefore you would always receive the first values. Yet it would seem like the iterator got rewinded somehow.

Upvotes: 0

hakre
hakre

Reputation: 198119

Like by the name of the class alone (NoRewindIterator), the manual has the following wording in specific:

NoRewindIterator - This iterator cannot be rewound.

And for the concrete method:

NoRewindIterator::rewind() - Prevents the rewind operation on the inner iterator.

This implies that the Iterator::rewind() method is not passed through to the inner iterator. Tests also show this, here is a simple one I've been running (code of all iterators not part of PHP are in the Iterator Garden):

$iterator = new RangeIterator(1, 1);
$debug    = new DebugIteratorDecorator($iterator);
$noRewind = new NoRewindIterator($debug);

echo "first foreach:\n";

foreach ($noRewind as $value) {
    echo "iteration value: $value\n";
}

In this code, the debug-iterator prints (echoes) iteration information on the fly:

first foreach:
Iterating (RangeIterator): #0 valid()
Iterating (RangeIterator): #0 parent::valid() is TRUE
Iterating (RangeIterator): #0 current()
Iterating (RangeIterator): #0 parent::current() is 1
iteration value: 1
Iterating (RangeIterator): #1 next()
Iterating (RangeIterator): #1 after parent::next()
Iterating (RangeIterator): #1 valid()
Iterating (RangeIterator): #1 parent::valid() is FALSE

As this shows, $iterator->rewind() is never called.

This also makes sense for the same reasons given in a related question: Why must I rewind IteratorIterator. The NoRewindIterator extends from IteratorIterator and different to it's parent class, the getInnerIterator() method returns an Iterator and not a Traversable.

This change allows you to initialize the rewind when you need to:

echo "\n\$calling noRewind->getInnerIterator()->rewind():\n";

$noRewind->getInnerIterator()->rewind();

echo "\nsecond foreach:\n";

foreach ($noRewind as $value) {
    echo "iteration value: $value\n";
}

Exemplary debug output again:

$calling noRewind->getInnerIterator()->rewind():
Iterating (RangeIterator): #0 rewind()
Iterating (RangeIterator): #0 after parent::rewind()

second foreach:
Iterating (RangeIterator): #0 valid()
Iterating (RangeIterator): #0 parent::valid() is TRUE
Iterating (RangeIterator): #0 current()
Iterating (RangeIterator): #0 parent::current() is 1
iteration value: 1
Iterating (RangeIterator): #1 next()
Iterating (RangeIterator): #1 after parent::next()
Iterating (RangeIterator): #1 valid()
Iterating (RangeIterator): #1 parent::valid() is FALSE

Knowing about these details then does allow to create a OneTimeRewindIterator for example:

/**
 * Class OneTimeRewindIterator
 */
class OneTimeRewindIterator extends NoRewindIterator
{
    private $didRewind = FALSE;

    public function rewind() {
        if ($this->didRewind) return;

        $this->didRewind = TRUE;
        $this->getInnerIterator()->rewind();
    }
}

Upvotes: 1

Related Questions