SPL: Using the iteratorAggregate interface

Warning: This blogpost has been posted over two years ago. That is a long time in development-world! The story here may not be relevant, complete or secure. Code might not be complete or obsoleted, and even my current vision might have (completely) changed on the subject. So please do read further, but use it with caution.
Posted on 04 Dec 2011
Tagged with: [ iterators ]  [ PHP ]  [ spl

The SPL is one of hardest things to grasp for most PHP developers. But why is this? The lack of documentation inside the manual, the fact that there are not many real-life examples, or maybe it’s just too hard? In this post I will try to explain a bit more about the “iteratorAggregate” interface. Together with its more famous brother Iterator, they are currently the two only implementations of the Traversable interface, which is needed for objects so they can be used within a standard foreach() loop. But why and when should we use the iteratorAggregate?

iteratorAggregate, because not everything is an iterator

Let’s say we have a book, and that books contains chapters. As a good-guy OO programmer, you probably will define a Book class and a separate Chapter class to deal with this. So is our Book-class an iterator? Well, it would be nice to be able to iterate over its chapters,  so yeah, it could be an iterator because we could use something like this:

foreach ($book as $chapter) {
  print $chapter->getTitle() . PHP_EOL;
}

Plus the fact that we are an iterator, we can do everything we could do with every other type of iterator. In the end, we can even throw in a library as well, which itself is an iterator (of a collection) of books:

foreach ($library as $book) {
  print "Author of ".$book->getTitle()." : ".$book->getAuthor() . PHP_EOL;
  foreach ($book as $chapter) {
    print "Chapter: ".$chapter->getTitle() . PHP_EOL;
  }
}

But in order to achieve all this, we must implement the iterator interface and deal with the iterator’s key(), current(), valid(), rewind() and next() methods inside our book class. We don’t really want this because this is not really the concern of the Book class. The Book class should just deal with the book and it should not contain methods for iterating chapters. We really like to move that somewhere else but on the other hand, we cannot really move it to our Chapter class since it would mean the Chapter would need to deal with multiple chapters. The Chapter class just deals with a single chapter.

So we will be introducing the iteratorAggregate which sounds far more complex than it actually is: The iteratorAggregate interface lets us deal with “aggregating” objects. The interface defines a single method called getIterator(), who’s only job is to return an iterator. One of the advantages is that an object that implemented IteratorAggregate, itself can be used through foreach(), just the same way a normal iterator can. Foreach() takes care of finding the actual iterator by calling the object’s getIterator() method.

<?php
/*
 * Example 1: Using iteratorAggregate
 */
// Simple class that represents our chapter
class Chapter {
    protected $_title;
    protected $_content;
    function __construct($title, $content) {
        $this->_title = $title;
        $this->_content = $content;
    }
    function getTitle() {
        return $this->_title;
    }
    function getContent() {
        return $this->_content;
    }
}
// Our book class. We can still iterate over our chapters with the
// help of the iteratorAggregate implementation.
class Book implements IteratorAggregate {
    protected $_title;
    protected $_author;
    protected $_chapters;
    function __construct($title, $author) {
        $this->_title = $title;
        $this->_author = $author;
        $this->_chapter = array();
    }
    // This method must be implemented. It will return the actual iterator
    function getIterator()
    {
        // This will return the articles. Since they are inside an array, we
        // can use the standard array-iterator.
        return new ArrayIterator($this->_chapters);
    }
    // Add a new chapter to this book
    function addChapter(Chapter $chapter) {
        $this->_chapters[] = $chapter;
    }
    function getTitle() {
        return $this->_title;
    }
    function getAuthor() {
        return $this->_author;
    }
}
// Create a new book and add chapters to it
$book = new Book("About the iteratorAggregate", "Joshua Thijssen");
$book->addChapter(new Chapter("Foreword", "This is the introduction"));
$book->addChapter(new Chapter("Chapter 1", "Content"));
$book->addChapter(new Chapter("Chapter 2", "Content"));
print "All chapters from the book '" . $book->getTitle() . "', written by ".$book->getAuthor()." :" . PHP_EOL;
foreach ($book as $chapter) {
    print "- " . $chapter->getTitle() . PHP_EOL;
}
// Add another chapter to our book
$book->addChapter(new Chapter("Epilogue", "Famous last words"));
print "All chapters from the book '" . $book->getTitle() . "', written by ".$book->getAuthor()." :" . PHP_EOL;
foreach ($book as $chapter) {
    print "- " . $chapter->getTitle() . PHP_EOL;
}

As you can see we define two classes: Book and Chapter. The Chapter class by itself isn’t capable of iterating. This is not needed since we let our book deal with this. The book class implements iteratorAggregate. Now, since we didn’t implement iterator, but iteratorAggregate, we don’t need to implement our key(), valid(), next(), current(), rewind() methods. We only need a getIterator() method that will return the actual iterator. In our case, it’s an arrayIterator over our chapters, since our chapters are stored as Chapter objects inside an array.

At this point we have separated the concerns of the classes and have a “clean” Book and Chapter class. Makes things easier, testable and in the end, more maintainable.

Further improvements

Where should we add a method on how many chapters there are inside the book? We could add a getChapterCount() method to our Book class, or even let it implement countable so we could use PHP’s standard count() function for it:

class Book implements IteratorAggregate, Countable {
    ...
    function count() {
        return count($this->_chapters);
    }
}

print "Number of chapters in our book: " . count($book) . PHP_EOL;

Since we now have an iterator, it’s easy to add our own filtering, for instance, limiting the foreach() to the first 2 chapters:

/*
 * Example 2: Using iteratorIterator to apply filtering
 */

// "change" our iteratoraggregate into a iterator.
$it = new IteratorIterator($book);
// Display the first two chapters only
$it = new LimitIterator($it, 0, 2);

foreach ($it as $chapter) {
    print "- " . $chapter->getTitle() . PHP_EOL;
}

In a sense, the  iteratorIterator does nothing really much more than stop letting you worry about if you are supplying an Iterator or an IteratorAggregate, since most filters need an iterator as input (and obviously, our $book isn’t. It’s easy to extend functionality this way without even having to touch chapters or book objects. It’s easy to setup, it’s maintainable and we can test our code without any problem.

« Thanks to Stephan Hochdörfer for some help on the examples »