Article

Introducing PHP 5's Standard Library

Page: 1 2

Admiring the Tree

Now you've seen how to write a basic Iterator, it's worth summarizing the interfaces and classes offered internally by the SPL extension, so that you know what their jobs are. This list may change in future, but it summarizes what's on offer right now.

Interfaces

  • Traversable: as mentioned above, this is an Iterator interface for PHP internals. Unless you're writing an extension, ignore this.
  • Iterator: as you've seen, this defines the basic methods to iterate forward through a collection.
  • IteratorAggregate: if you would rather implement the Iterator separately from your "collection" object, implementing Iterator Aggregate will allow you to delegate the work of iteration to a separate class, while still enabling you to use the collection inside a foreach loop.
  • RecursiveIterator: this defines methods to allow iteration over hierarchical data structures.
  • SeekableIterator: this defines a method to search the collection that the Iterator is managing.
  • ArrayAccess: here's another magic interface with a special meaning for the Zend engine. Implementing this allows you to treat your object like an array with normal PHP array syntax (more on this below).

Classes

  • ArrayIterator: this Iterator can manage both native PHP arrays and the public properties of an object (more on this shortly).
  • ArrayObject: this unifies arrays and objects, allowing you to iterate over them and use array syntax to access the contents. See "Objects as Arrays" below (we'll grow our own class with similar behaviour).
  • FilterIterator: this is an abstract class that can be extended to filter the elements that are being iterated over (perhaps removing unwanted elements for a search).
  • ParentIterator: when using a ResursiveIterator, the ParentIterator allows you to filter out elements that do not have children. If, for example, you have a CMS in which documents can be placed anywhere under a tree of categories, the ParentIterator would allow you to recurse the tree but display only the "category nodes", omitting the documents that appear under each category.
  • LimitIterator: this class allows you to specify a range of elements to Iterator over, starting with a key offset and specifying a number of elements to access from that point. The concept is the same as the LIMIT clause in MySQL.
  • CachingIterator: this manages another Iterator (which you pass to its constructor). It allows you to check whether the inner Iterator has more elements, using the hasNext() method, before actually advancing with the next() method. Personally, I'm not 100% sure about the name; perhaps LookAheadIterator would be more accurate?
  • CachingRecursiveIterator: this is largely the same as the CachingIterator, but allows iteration over hierarchical data structures.
  • DirectoryIterator: to iterate over a directory in a file system, this Iterator provides a bunch of useful methods like isFile() and isDot() that save a lot of hassle.
  • RecursiveDirectoryIterator: this class allows iteration over a directory structure so that you can descend into subdirectories.
  • SimpleXMLIterator: this makes SimpleXML even simpler! Currently, the best examples can be found with the SPL tests -- see the files beginning "sxe_*"
  • RecursiveIteratorIterator: this helps you do cool stuff like "flatten" a hierarchical data structure so that you can loop through it with a single foreach statement, while still preserving knowledge of the hierarchy. This class could be very useful for rendering tree menus, for example.

To see it in action, try using the DirectoryTreeIterator (which extends RecursiveIteratorIterator), like so:

$DirTree = new DirectoryTreeIterator('/some/directory');  
 
foreach ($DirTree as $node) {  
   echo "$node\n";  
}

That summarizes the core classes and interfaces that the SPL extension defines today.

Objects as Arrays

You've already seen how implementing the Iterator interface allows you to "overload" the foreach construct. The SPL extension has some more surprises in store, though, beginning with the ArrayAccess interface. Implementing this interface with a class allows you treat objects of that class as arrays from the perspective of PHP syntax.

Here's an example:

/**  
* A class that can be used like an array  
*/  
class Article implements ArrayAccess {  
 
 public $title;  
 
 public $author;  
 
 public $category;  
 
 function __construct($title,$author,$category) {  
   $this->title = $title;  
   $this->author = $author;  
   $this->category = $category;  
 }  
 
 /**  
 * Defined by ArrayAccess interface  
 * Set a value given it's key e.g. $A['title'] = 'foo';  
 * @param mixed key (string or integer)  
 * @param mixed value  
 * @return void  
 */  
 function offsetSet($key, $value) {  
   if ( array_key_exists($key,get_object_vars($this)) ) {  
     $this->{$key} = $value;  
   }  
 }  
 
 /**  
 * Defined by ArrayAccess interface  
 * Return a value given it's key e.g. echo $A['title'];  
 * @param mixed key (string or integer)  
 * @return mixed value  
 */  
 function offsetGet($key) {  
   if ( array_key_exists($key,get_object_vars($this)) ) {  
     return $this->{$key};  
   }  
 }  
 
 /**  
 * Defined by ArrayAccess interface  
 * Unset a value by it's key e.g. unset($A['title']);  
 * @param mixed key (string or integer)  
 * @return void  
 */  
 function offsetUnset($key) {  
   if ( array_key_exists($key,get_object_vars($this)) ) {  
     unset($this->{$key});  
   }  
 }  
 
 /**  
 * Defined by ArrayAccess interface  
 * Check value exists, given it's key e.g. isset($A['title'])  
 * @param mixed key (string or integer)  
 * @return boolean  
 */  
 function offsetExists($offset) {  
   return array_key_exists($offset,get_object_vars($this));  
 }  
 
}

Filename: arrayaccess1.php

The four methods that begin with "offset" are defined by the ArrayAccess interface that I'm implementing.

Note that I've used a couple of PHP runtime tricks to make life easier, such as checking that object variables have been defined by introspection:

function offsetSet($key, $value) {  
   if ( array_key_exists($key,get_object_vars($this)) ) {

I've also referenced them indirectly, using a variable that holds their names:

$this->{$key} = $value;

This example gets interesting when you see how this class can now be used:

// Create the object  
$A = new Article('SPL Rocks','Joe Bloggs', 'PHP');  
 
// Check what it looks like  
echo 'Initial State:<pre>';  
print_r($A);  
echo '</pre>';  
 
// Change the title using array syntax  
$A['title'] = 'SPL _really_ rocks';  
 
// Try setting a non existent property (ignored)  
$A['not found'] = 1;  
 
// Unset the author field  
unset($A['author']);  
 
// Check what it looks like again  
echo 'Final State:<pre>';  
print_r($A);  
echo '</pre>';

Apart from the first line, in which I create the object, the code is valid syntax for a native PHP array. Here's the output:

Initial State:  
 
Article Object  
(  
   [title] => SPL Rocks  
   [author] => Joe Bloggs  
   [category] => PHP  
)  
 
Final State:  
 
Article Object  
(  
   [title] => SPL _really_ rocks  
   [category] => PHP  
)

Note that I could add logic to manipulate the data as it's being read by modifying the offsetGet() method as follows:

function offsetGet($key) {  
   if ( array_key_exists($key,get_object_vars($this)) ) {  
     return strtolower($this->{$key});  
   }  
 }

This would convert all values to lower-case.

To make the object iterable, using foreach or otherwise, I can now take advantage of the SPL's ArrayIterator class, combined with the IteratorAggregate interface.

As I mentioned before, the IteratorAggregate interface is used when you don't want to embed the Iterator logic in the object that contains the data over which you want to iterate. This can be a useful way to keep this logic divided but, more interestingly, it allows you to re-use existing iterators.

To begin, I modify the first line of the Article class to declare the interface implementation:

class Article implements ArrayAccess, IteratorAggregate {

Now, I need to add one extra method: getIterator(), which returns the object used for iteration:

/**  
 * Defined by IteratorAggregate interface  
 * Returns an iterator for for this object, for use with foreach  
 * @return ArrayIterator  
 */  
 function getIterator() {  
   return new ArrayIterator($this);  
 }

With that done, I can loop through the properties defined in the class:

$A = new Article('SPL Rocks','Joe Bloggs', 'PHP');  
 
// Loop (getIterator will be called automatically)  
echo 'Looping with foreach:<pre>';  
foreach ( $A as $field => $value ) {  
 echo "$field : $value<br>";  
}  
echo '</pre>';  
 
// Get the size of the iterator (see how many properties are left)  
echo "Object has ".sizeof($A->getIterator())." elements";

Filename: arrayaccess2.php

Here's what it displays:

$A = new Article('SPL Rocks','Joe Bloggs', 'PHP');  
 
// Loop (getIterator will be called automatically)  
echo 'Looping with foreach:<pre>';  
foreach ( $A as $field => $value ) {  
 echo "$field : $value<br>";  
}  
echo '</pre>';  
 
// Get the size of the iterator (see how many properties are left)  
echo "Object has ".count($A->getIterator())." elements";

This gives me:

Looping with foreach:  
 
title : SPL Rocks  
author : Joe Bloggs  
category : PHP  
 
Object has 3 elements

Notice that I was also able to use the count function on the object to find out how many elements it has. This could allow me to use other loop constructs without needing to call the Iterator methods:

$size = count($A);  
for($i = 0; $i < $size; $i++ ) {  
   echo $A[$i]."\n";  
}

What doesn't (yet) work is the application of PHP's array functions to the object (you'll get complaints about it not being an array). However, so long as you're not doing type checking with something like is_array(), you should be able to reuse any part of your own code that was written to expect an array.

The Big Deal

I hope you're getting the creeping feeling that the SPL extension is something "big" in terms of making life easier for PHP developers. Personally, I'm highly impressed by what SPL offers already, and give due credit to Marcus Boerger for making it possible. It's also settled some of the doubts I've had about interface in PHP5, having proved their usefulness as mechanism by which we can specify a contract between the PHP engine and code written in PHP, allowing the alteration of the semantics of PHP's syntax to great effect.

Perhaps the most important aspect of what the SPL does today is that it encourages the use of standards, first by defining a set of APIs that are "always on" in PHP5 (so why not use them?), and, second, with the additional "carrot" of being able to overload PHP syntax and constructs like foreach.

Assuming we all agree to use the classes and interfaces provided by the SPL, projects can begin to converge on them. For example, consider HTML_TreeMenu, a PEAR library designed to enable the generation of Javascript-based tree menus in HTML. Right now, it leaves a fair amount of work up to the developer. Here's an example of what's required to draw a tree from a directory structure with HTML_TreeMenu today:

require_once 'HTML/TreeMenu.php';  
$map_dir = 'c:/windows';  
$menu  = new HTML_TreeMenu('menuLayer', 'images', '_self');  
$menu->addItem(recurseDir($map_dir));  
 
function &recurseDir($path) {  
   if (!$dir = opendir($path)) {  
       return false;  
   }  
   $files = array();  
   $node = &new HTML_TreeNode(basename($path), basename($path), 'folder.gif');  
   while (($file = readdir($dir)) !== false) {  
       if ($file != '.' && $file != '..') {  
           if (@is_dir("$path/$file")) {  
               $addnode = &recurseDir("$path/$file");  
           } else {  
               $addnode = &new HTML_TreeNode($file, $file, 'document2.png');  
           }  
           $node->addItem($addnode);  
       }  
   }  
   closedir($dir);  
   return $node;  
}  
 
echo $menu->printMenu();

In other words, it's left to us to prepare the data in the correct order and build the tree. Instead, HTML_Treemenu could provide a mechanism by which we could register the data structure, then leave it to do the iterating for us. The above example might be reduced to:

require_once 'HTML/TreeMenu.php';  
$map_dir = 'c:/windows';  
$menu  = new HTML_TreeMenu('menuLayer', 'images', '_self');  
 
// Register the tree data structure  
$menu->registerTree(new new RecursiveDirectoryIterator($map_dir);  
 
echo $menu->printMenu();

If there isn't a RecursiveIterator on hand that suits your problem, you could always implement your own, leaving HTML_Treemenu to take advantage of type hints to make sure you're giving it what it needs.

More Standards Please!

The question is, what else can be standardized? The extension is called the "Standard PHP Library", after all. After a brief email interchange with Marcus Boerger, I'm happy to report some of the things he has in mind for the future (depending on the availability of time and helping hands):

  • Standard exceptions such as RuntimeException and OutOfBoundsException (something equivalent to Python's built-in exceptions, where exceptions are named for the problem they'll be used to flag).
  • Some notion of Design by Contract TM (which will likely introduce new language constructs like requires() and ensures(), plus associated exceptions).
  • More pattern implementations, where possible. The Observer pattern may be a particularly good candidate, for example, but to quote some of the questions Marcus has there: The problem of observable is how to do it without multiple inheritance? The thing being observed needs to have a container of observers. But we only have interfaces. Also, how about dealing with Objects that should act as Observer for different Observable's? Should I pass the originating Observable? And, if so, how will it identify itself? So you see there's a lot to think about. And areas where I need more input and use cases.
  • Some notion of dependency injection (a pattern for managing objects that depend on other objects). To that aim, the new reflection API needs to become more accessible.

If you're interested in hearing more or talking to Marcus directly, the place to catch him is at the International PHP Conference this coming November in Frankfurt, Germany, where he'll be doing a talk on SPL.

Enough from me. Get iterating!

If you liked this article, share the love:
Print-Friendly Version Suggest an Article

Sponsored Links

Rate This Article

  • 1
    Poor
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
    Great

Comment on This Article

Have something to say?

Post A Comment

You need to be a member of the SitePoint Forums to comment on this post. Sign Up

Already a member? Post using your SitePoint Forums account: