Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How should PHP's Iterator methods valid(), current(), and next() behave?

Tags:

php

I am using PHP 5.3.6 from MAMP.

I have a use case where it would be best to use PHP's Iterator interface methods, next(), current(), and valid() to iterate through a collection. A foreach loop will NOT work for me in my particular situation. A simplified while loop might look like

<?php
while ($iter->valid()) {
  // do something with $iter->current()
  $iter->next();
}

Should the above code always work when $iter implements PHP's Iterator interface? How does PHP's foreach keyword deal with Iterators?

The reason I ask is that the code I am writing may be given an ArrayIterator or a MongoCursor. Both implement PHP's Iterator interface but they behave differently. I would like to know if there is a bug in PHP or PHP's Mongo extension.

ArrayIterator::valid() returns true before any call to next() -- immediately after the ArrayIterator is created.

MongoCursor::valid() only returns true after the first call to next(). Therefore the while loop above will never execute.

At risk of being verbose, the following code demonstrates these assertions:

<?php

// Set up array iterator
$arr = array("first");
$iter = new \ArrayIterator($arr);

// Test array iterator
echo(($iter->valid() ? "true" : "false")."\n"); // Echoes true
var_dump($iter->current()."\n");                // "first"
$iter->next();
echo(($iter->valid() ? "true" : "false")."\n"); // Echoes false


// Set up mongo iterator
$m = new \Mongo();
$collection = $m->selectDB("iterTest")->selectCollection("mystuff");
$collection->drop(); // Ensure collection is empty
$collection->insert(array('a' => 'b'));
$miter = $collection->find(); // should find one object

// Test mongo iterator
echo(($miter->valid() ? "true" : "false")."\n"); // Echoes false

$miter->next();

echo(($miter->valid() ? "true" : "false")."\n"); // Echoes true
var_dump($miter->current());                     // Array(...)

Which implementation is correct? I found little documentation to support either behavior, and the official PHP documentation is either ambiguous or I'm reading it wrong. The doc for Iterator::valid() states:

This method is called after Iterator::rewind() and Iterator::next() to check if the current position is valid.

This would suggest that my while loop should first call next().

Yet the PHP documentation for Iterator::next states:

This method is called after each foreach loop.

This would suggest that my while loop is correct as written.

To summarize - how should PHP iterators behave?

like image 607
meva Avatar asked Mar 19 '12 20:03

meva


People also ask

Which of the following is the iterator function in PHP?

An iterable is any value which can be looped through with a foreach() loop. The iterable pseudo-type was introduced in PHP 7.1, and it can be used as a data type for function arguments and function return values.

Which of the following is the iterator function?

Iterator in Java is used to traverse each and every element in the collection. Using it, traverse, obtain each element or you can even remove. ListIterator extends Iterator to allow bidirectional traversal of a list, and the modification of elements.

How important is the PHP iterator in an application?

Iterators encourage you to process data iteratively, instead of buffering it in memory. While it is possible to do this without iterators, the abstractions they provide hide the implementation which makes them really easy to use.


2 Answers

This is an interesting question. I'm not sure why a foreach won't work for you, but I have some ideas.

Take a look at the example given on the Iterator interface reference page. It shows the order in which PHP's internal implementation of foreach calls the Iterator methods. In particular, notice that when the foreach is first set up, the very first call is to rewind(). This example, though it's not well-annotated, is the basis for my answer.

I'm not sure why a MongoCursor would not return true for valid() until after next() is called, but you should be able to reset either type of object by calling rewind() prior to your loop. So you would have:

// $iter may be either MongoCursor or ArrayIterator

$iter->rewind();
while( $iter->valid() ){
    // do something with $iter->current()
    $iter->next();
}

I believe this should work for you. If it does not, the Mongo class may have a bug in it.

Edit: Mike Purcell's answer correctly calls out that ArrayIterator and Iterator are not the same. However, ArrayIterator implements Iterator, so you should be able to use rewind() as I show above on either of them.

like image 117
Jazz Avatar answered Oct 28 '22 17:10

Jazz


Subclassing any Iterator and echo'ing when it's called will tell you how it behaves.

Example (demo)

class MyArrayIterator extends ArrayIterator
{
    public function __construct ($array)
    {
        echo __METHOD__, PHP_EOL;
        parent::__construct($array);
    }
    …
}

foreach (new MyArrayIterator(range(1,3)) as $k => $v) {
    echo "$k => $v", PHP_EOL;
}

Output

MyArrayIterator::__construct
MyArrayIterator::rewind
MyArrayIterator::valid
MyArrayIterator::current
MyArrayIterator::key
0 => 1
MyArrayIterator::next
MyArrayIterator::valid
MyArrayIterator::current
MyArrayIterator::key
1 => 2
MyArrayIterator::next
MyArrayIterator::valid
MyArrayIterator::current
MyArrayIterator::key
2 => 3
MyArrayIterator::next
MyArrayIterator::valid

This is equivalent to doing

$iterator = new MyArrayIterator(range(1,3));
for ($iterator->rewind(); $iterator->valid(); $iterator->next()) {
    echo "{$iterator->key()} => {$iterator->current()}", PHP_EOL;
}

The sequence in which the methods are called is identical to a custom Iterator:

class MyIterator implements Iterator
{
    protected $iterations = 0;
    public function current()
    {
        echo __METHOD__, PHP_EOL;
        return $this->iterations;
    }
    public function key ()
    {
        echo __METHOD__, PHP_EOL;
        return $this->iterations;
    }
    public function next ()
    {
        echo __METHOD__, PHP_EOL;
        return $this->iterations++;
    }
    public function rewind ()
    {
        echo __METHOD__, PHP_EOL;
        return $this->iterations = 0;
    }
    public function valid ()
    {
        echo __METHOD__, PHP_EOL;
        return $this->iterations < 3;
    }
}
foreach (new MyIterator as $k => $v) {
    echo "$k => $v", PHP_EOL;
}
like image 22
Gordon Avatar answered Oct 28 '22 17:10

Gordon