Tuesday, 11 September 2012

Iterators


Recently I came across a situation where I need to iterate over multiple result sets. Since SPL has a decent collection of iterators I began my search there. So I came across the MultipleIterator thinking it would solve all my problems. It didn't.
The problem with the MultipleIterator is that it doesn't provide a continuous stream of elements of all the attached iterators, instead it 'groups' them. Per iteration you get an array of all n-th elements for all attached iterators.
Given the following piece of code:

<?php
$iter 
= new MultipleIterator();
$iter->attachIterator(new ArrayIterator(range(05)));
$iter->attachIterator(new ArrayIterator(range(1015)));

foreach (
$iter as $row) {
    
var_dump($row);
}


The output is:

array(2) {
  [0]=>
  int(0)
  [1]=>
  int(10)
}
array(2) {
  [0]=>
  int(1)
  [1]=>
  int(11)
}
array(2) {
  [0]=>
  int(2)
  [1]=>
  int(12)
}
array(2) {
  [0]=>
  int(3)
  [1]=>
  int(13)
}
array(2) {
  [0]=>
  int(4)
  [1]=>
  int(14)
}
array(2) {
  [0]=>
  int(5)
  [1]=>
  int(15)
}

Not exactly what i want. And even worse, all iterators need to be of the same length. It just doesn't cover my use case where I simply want to group a bunch of iterators and interface with them as if it were a single one.

It seems the SPL doesn't provide a solution to my use case, so I wrote my own. This is the implementation I ended up with:

<?php 
class MultiIterator 
implements Iterator 
{
    
/**
     * Holds the stack of iterators
     * @var array
     */
    
protected $iterators = array();
    
/**
     * Holds the index to the current interator
     * @var integer
     */
    
protected $current_iterator_idx;
    
/**
     * Add a new iterator to the stack
     * @param Iterator $iterator
     */
    
public function appendIterator(Iterator $iterator)
    {
        
$this->iterators[] = $iterator;
    }
    
/**
     * Returns the currently used iterator
     * @return Iterator
     */
    
public function iterator()
    {
        return (isset(
$this->iterators[$this->current_iterator_idx])) ? $this->iterators[$this->current_iterator_idx] : false;
    }
    
/**
     * (non-PHPdoc)
     * @see Iterator::current()
     */
    
public function current()
    {
        return 
$this->iterator()->current();
    }
    
/**
     * (non-PHPdoc)
     * @see Iterator::next()
     */
    
public function next()
    {
        
//does nothing
        
return $this->iterator()->next();
    }
    
/**
     * (non-PHPdoc)
     * @see Iterator::key()
     */
    
public function key()
    {
        return 
$this->iterator()->key();
    }
    
/**
     * (non-PHPdoc)
     * @see Iterator::valid()
     */
    
public function valid()
    {
        if (!
count($this->iterators)) {
            return 
false;
        }
        
//check if current iterator is valid
        
$valid $this->iterator()->valid();
        if (
false === $valid) { //this iterator has dried up, try and find a new one
            //increment the index to find a new iterator
            
$this->current_iterator_idx++;

            
//now check if we have a new iterator
            
if (false !== $this->iterator()) {
                
//we have a new iterator, valid or not
                
return $this->iterator()->valid();
            } else {
                
//no more iterators
                
return false;
            }
        }
     
        
//apparantly, the current iterator is still valid
        
return $valid;
    }
    
/**
     * (non-PHPdoc)
     * @see Iterator::rewind()
     */
    
public function rewind()
    {
        
//reset state
        
$this->current_iterator_idx 0;
        
array_walk($this->iterators, function($iter) {
            
$iter->rewind();
        });
    }
}


With the expected usage and output:

<?php
$iter 
= new MultiIterator();
$iter->appendIterator(new ArrayIterator(range(06)));
$iter->appendIterator(new ArrayIterator(range(1015)));

foreach (
$iter as $row) {
    
var_dump($row);
}



int(0)
int(1)
int(2)
int(3)
int(4)
int(5)
int(6)
int(10)
int(11)
int(12)
int(13)
int(14)
int(15)

Problem solved.