The Iterator Pattern, concluded - SPL Iterator (
Page 3 of 4 )
No chapter on the Iterator design pattern and PHP would be complete without discussing the "Standard PHP Library" (SPL) iterator.
The while loop structure used so far is very compact and usable, but PHP coders may be more comfortable with the foreach structure for array iteration. Wouldn't it be nice to use a collection directly in a foreach loop? That's exactly what the SPL iterator is for.
(Even though this chapter has been written entirely for PHP5, the following SPL code is the only code that works solely in PHP5, and then only if you've compiled PHP 5 with SPL enabled.) Harry Fuecks wrote a nice article introducing the SPL and covering the SPL iterator; see http://www.sitepoint.com/article/php5-standard-library.
Using SPL is essentially a completely different way to implement iteration, so let's start over with a new unit test case and a new class, the ForeachableLibrary.
class SplIteratorTestCase extends UnitTestCase {
protected $lib;
function setup() {
$this->lib = new ForeachableLibrary;
$this->lib->add(new Media('name1', 2000));
$this->lib->add(new Media('name2', 2002));
$this->lib->add(new Media('name3', 2001));
}
function TestForeach() {
$output = '';
foreach($this->lib as $item) {
$output .= $item->name;
}
$this->assertEqual('name1name2name3', $output);
}
}
ForeachableLibrary is the collection that implements the SPL Iterator interface. You have to implement five functions to create an SPL iterator: current(), next(), key(), valid(), and rewind(). key() returns the current index of your collection. rewind() is like reset(): iteration restarts at the start of your collection.
class ForeachableLibrary
extends Library
implements Iterator {
protected $valid;
function current() {
return current($this->collection);
}
function next() {
$this->valid = (false !== next($this->collection));
}
function key() {
return key($this->collection);
}
function valid() {
return $this->valid;
}
function rewind() {
$this->valid = (false !== reset($this->collection));
}
}
Here, the code we just implements the required functions working on the $collection attribute. (If you don't implement all five functions and you add the implements Iterator to your class definition, PHP will generate a fatal error.) The tests are "green," so everything is happy.
There's just one problem: the implementation is limited to one style of iteration—sorting or filtering is impossible.
Can anything be done to rectify this? Yes! Apply what you learned from the Strategy pattern (see Chapter 7) and delegate the SPL iterator's five functions to another object.
This is a test for PolymorphicForeachableLibrary.
class PolySplIteratorTestCase extends UnitTestCase {
protected $lib;
function setup() {
$this->lib = new PolymorphicForeachableLibrary;
$this->lib->add(new Media('name1', 2000));
$this->lib->add(new Media('name2', 2002));
$this->lib->add(new Media('name3', 2001));
}
function TestForeach() {
$output = '';
foreach($this->lib as $item) {
$output .= $item->name;
}
$this->assertEqual('name1name2name3', $output);
}
}
The only difference between this case and the test for SplIteratorTestCase is the class of the
$this->lib attribute created in the setUp() method. That makes sense: the two classes must behave identically.
Here's PolymorphicForeachableLibrary.
class PolymorphicForeachableLibrary
extends Library
implements Iterator {
protected $iterator;
function current() {
return $this->iterator->current();
}
function next() {
return $this->iterator->next();
}
function key() {
return $this->iterator->key();
}
function valid() {
return $this->iterator->valid();
}
function rewind() {
$this->iterator =
new StandardLibraryIterator($this->collection);
$this->iterator->rewind();
}
}
Library is extended to get the collection manipulation methods. The SPL methods are added, too, all delegating to the $iterator attribute, which is created in rewind(). Below is the code for the StandardLibraryIterator.
class StandardLibraryIterator {
protected $valid;
protected $collection;
function __construct($collection) {
$this->collection = $collection;
}
function current() {
return current($this->collection);
}
function next() {
$this->valid = (false !== next($this->collection));
}
function key() {
return key($this->collection);
}
function valid() {
return $this->valid;
}
function rewind() {
$this->valid = (false !== reset($this->collection));
}
}
This code should look familiar: essentially, it's a copy of the five SPL functions from the ForeachableLibrary class. The tests pass.
OK, the code is more complex now, but how does it support additional iterator types? Let's add a test for a "released" version of the iterator to see how additional iterator types work in this design.
class PolySplIteratorTestCase extends UnitTestCase {
// ...
function TestReleasedForeach() {
$this->lib->add(new Media('second', 1999));
$this->lib->add(new Media('first', 1989));
$output = array();
$this->lib->iteratorType('Released');
foreach($this->lib as $item) {
$output[] = $item->name .'-'. $item->year;
}
$this->assertEqual(
'first-1989 second-1999 name1-2000 name3-2001 name2-2002'
,implode(' ',$output));
}
}
This test case above should look familiar, too, as it's very similar to the previous "release" iterator, but using the foreach control structure to loop.
class PolymorphicForeachableLibrary
extends Library
implements Iterator {
protected $iterator_type;
protected $iterator;
function __construct() {
$this->iteratorType();
}
function iteratorType($type=false) {
switch(strtolower($type)) {
case 'released':
$this->iterator_type = 'ReleasedLibraryIterator';
break;
default:
$this->iterator_type = 'StandardLibraryIterator';
}
$this->rewind();
}
// ...
function rewind() {
$type = $this->iterator_type;
$this->iterator = new $type($this->collection);
$this->iterator->rewind();
}
}
The new iteratorType() method lets you switch which style of iterator you want to use. (Since the iterator type isn't chosen during the instantiation of the object and because you can choose a different iterator type on-the-fly by calling the iteratorType() method again, the code is actually implementing the State pattern, rather than the Strategy pattern.)
class ReleasedLibraryIterator
extends StandardLibraryIterator {
function __construct($collection) {
usort($collection
,create_function('$a,$b','return ($a->year - $b->year);'));
$this->collection = $collection;
}
}
You can easily implement ReleasedLibraryIterator by extending StandardLibraryIterator and overriding the constructor to add the sorting of the incoming array. And with that you have a working PolymorphicForeachableLibrary.