SPL: Using the iteratorAggregate interface

December 4, 2011
By

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:

<?php
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:

<?php
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.

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:

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 >>

Share

Tags: , ,

9 Responses to SPL: Using the iteratorAggregate interface

  1. jacoop
    December 6, 2011 at 15:11

    Maybe beyond the scope of this tutorial, but isn’t it better to create the iterator inside the constructor? This way there’s no need to create a new instance every time getIterator() is called.

    Hmmm… while typing this comment I realize an external could easily erase a chapter, so never mind ;)

    Nice article, always a pleasure to read about how to use SPL.

    • Joshua Thijssen
      December 6, 2011 at 17:42

      You really can’t in this case. Suppose you implement lazyloading inside the getIterator(), or initialize through the constructor.
      When we iterator for the first time in our example, there is no problem. But when we add another chapter and iterate again, it will continue to use the “old” iterator that doesn’t have the latest chapter.

      See my example at:
      https://gist.github.com/1438895

  2. December 6, 2011 at 16:11

    Nice! I knew the syntactic differences between the two; now I know the semantic difference as well. Good example illustrating separation of concerns. I can definitely picture my own code where I’ve used Iterator that probably should have been IteratorAggregate. Thanks for the post.

  3. jacoop
    December 6, 2011 at 23:07

    I get your point, but to be clear, your example differs from what I meant.

    function __construct($title, $author) {
    $this->_title = $title;
    $this->_author = $author;
    $this->_chapters = new ArrayIterator();
    }

    function getIterator()
    {
    return $this->_chapters;
    }

    This solves the poblem you’ve mentioned, right?

    • Joshua Thijssen
      December 7, 2011 at 08:28

      In that case yes. Since ArrayIterator implements ArrayAccess, the $this->_chapters[] behaves the same way as is it was a “normal” array. However, I do not see this happening often. Normally the getIterator() will return a new iterator instead of reusing a current one (I’ve checked symfony2/doctrine2 code and every getIterator returns a new object). Initiantiating an iterator isn’t a very big deal compared to the actual iterating over the objects so a performance-optimization cannot be a reason (in fact, I think it might be even slower to add your elements directory to the ArrayIterator). However, I’ll try to find out the actual reason (if there is any).

  4. Sandor
    December 7, 2011 at 00:13

    Nice article, but I think count($book) is not that expressive, it doesn’t give enough hint what it exactly counts

    • Joshua Thijssen
      December 7, 2011 at 08:10

      Have to agree with you on this one. count($book) could also be counting the pages, words, letters, or anything else. But if our $book object would be instantiated from an ChapterCollection instead, it would start to make more sense. But that would probably mean that we’re stepping a little bit in Java territory here :) A countable interface would make more sense if you have a database-resultset iterator, or an “easteregg-basket” class where count($basket) returns the number of eggs. In the book-case, I’d probably go for an getChapterCount(), getWordCount() methods probably..

  5. lingtalfi
    November 8, 2012 at 09:57

    Thank you for pointing this out.