WEB Advent 2011 / Better Object-Oriented Arrays

I’ve been working on developer-facing software and SDKs in PHP for nearly a decade, and through the experience of supporting these developers, I’ve learned something interesting about the PHP community at-large. The majority of PHP developers have a very good understanding of native types (e.g., strings, arrays, integers, booleans). Since they’re the lowest common denominators of the PHP language, it’s generally pretty easy for developers to understand these types.

But the moment you introduce concepts like objects as return types, people suddenly can’t figure out how the darn thing works. It’s remarkably similar to when people forget how to drive the instant it starts raining. Most PHP software has trained us to expect native types as responses, but if you return something like a `SimpleXMLElement` object or some other sort of custom object, confusion erupts!

Stopping the Insanity with Collections

In an effort to find a better solution to the array versus object debacle, I started investigating the possibility of object-oriented arrays in PHP. This way, you can return an array-like object which behaves just like you’d expect an array to, but has all of the power of an object.

PHP has two classes called ArrayObject and ArrayIterator that are more like stubs than anything useful. What they provide, however, is a reasonably solid foundation for building a useful object-oriented array class on top of. These classes also implement a set of interfaces that give us a significant amount of functionality relatively for free.

Since the word array already has a specific meaning in PHP, I’ll call our new object-oriented array class the Collection class. The goal of this class is to behave identically to an array, but support a variety of methods like the kind you’d find in everything-is-an-object languages like Ruby and JavaScript.

Constructor

Let’s begin with our class definition and constructor:

class Collection implements IteratorAggregate, ArrayAccess, Countable, Serializable
{
	private $collection;
	private $iterator;

	public function __construct($value = array())
	{
		$this->collection = new ArrayObject($value, ArrayObject::ARRAY_AS_PROPS);
		$this->iterator = $this->collection->getIterator();
	}
}

Note: The IteratorAggregate interface extends the Traversable interface, which enables foreaching through the collection.

Interface Methods

Next, we need to implement all of the methods that are defined by the interfaces. These include:

  • current, key, next, rewind, seek, and valid from the Traversable interface.
  • getIterator from the IteratorAggregate interface.
  • offsetExists, offsetGet, offsetSet, and offsetUnset from the ArrayAccess interface.
  • count from the Countable interface.
  • serialize and unserialize from the Serializable interface.

For the most part, these can be implemented by simply calling out to the already-existing methods on the $this->collection and $this->iterator properties.

If you want your collections to be able to be serialized and unserialized, you’ll need to custom implement these methods. The trickiest thing to watch out for is handling cases where your collection contains a SimpleXMLElement element. Since SimpleXMLElement can’t be serialized, you’ll need to manually convert the object to an XML string on serialization, and re-parse it on unserialization.

Converting Types

So far, you’ll already be able to do things like this:

$data = new Collection(array(1, 2, 3, 4, 5, 'a', 'b', 'c'));
$numbers = new Collection();
$letters = new Collection();

foreach ($data as $entry)
{
	if (is_int($entry))
	{
		$numbers[] = $entry;
	}
	elseif (is_string($entry))
	{
		$letters[] = $entry;
	}
}

However, since array functions in PHP require the inputs to be real arrays, we’ll need to be able to easily export the collection to an old-school array.

public function to_array()
{
	return $this->collection->getArrayCopy();
}

Maybe you even want to add functionality to implement things like to_json(), to_yaml(), or to_object(). This is really easy to do with off-the-shelf tools found in PHP itself and popular third-party packages (such as Symfony YAML).

Magic Methods

What would an awesome collection class be without some magic?

// Support looking up nodes using $obj->key
// Also, call methods without parenthesis: $obj->method
public function __get($name)
{
	if (method_exists($this, $name))
	{
		return $this->$name();
	}
	elseif ($this->exists($name))
	{
		return $this[$name];
	}

	return null;
}

// Support setting values with $obj->key = $value
public function __set($name, $value)
{
	if (method_exists($this, $name))
	{
		return $this->$name($value);
	}

	$this[$name] = $value;
	return $this;
}

// Support cloning this object
public function __clone()
{
	$this->collection = clone $this->collection;
	$this->iterator = clone $this->iterator;
}

// Support echo $obj
public function __toString()
{
	print_r($this->collection->getArrayCopy());
}

Now What?

So, you’ve put together this class. Now what? Well, now you have an object that behaves like an array, can do all of the things that an array is designed to do, and has all of the advantages of being an object. Let’s look at what works and what doesn’t. (Pretend we’ve also taken the time to implement methods like first and last.)

$array = array(
	'key1' => 'value1',
	'key2' => 'value2',
	'key3' => 'value3',
	'key4' => 'value4',
	'key5' => 'value5'
);

$array[0]       // Error!
$array['key1']  // "value1"
$array->key1    // Error!
rewind($array)  // "value1"
end($array)     // "value5"
$array->first() // Error!
$array->first   // Error!
$array->last()  // Error!
$array->last    // Error!

array_values($array); // ["value1", "value2", "value3", "value4", "value5"]

Versus…

$collection = new Collection(array(
	'key1' => 'value1',
	'key2' => 'value2',
	'key3' => 'value3',
	'key4' => 'value4',
	'key5' => 'value5'
));

$collection[0]       // Error
$collection['key1']  // "value1"
$collection->key1    // "value1"
rewind($collection)  // "value1"
end($collection)     // "value5"
$collection->first() // "value1"
$collection->first   // "value1"
$collection->last()  //" value5"
$collection->last    // "value5"

array_values($collection->to_array); // ["value1", "value2", "value3", "value4", "value5"]

You can now access collection keys as indexes (like arrays) or properties (like objects). The advantage here is that you can extend your class even further by implementing methods such as push, pop, first, last, flatten, slice, sort_by, reduce, each, map, min, max, every, any, grep, ungrep, and more, all by manipulating the value of $this->collection. You can’t do that with an old-school array!

Homework

To make a really robust collection class, I would recommend borrowing as many ideas as possible from everything-is-an-object languages like Ruby and JavaScript. Once you have your powerful collection class fully implemented, you’ll never have to worry about needle-haystack issues ever again, and your developer-customers will have far less confusion functionality when using your code.

At some point, maybe we can do the same thing with object-oriented strings. :-)

Developer Gift

I have two ideas.

When I was working on SimplePie, the developers would post our Amazon wishlists online in case anybody wanted to get us something in appreciation for the software. I would randomly get cool presents left on my doorstep throughout the course of the year. It was kind of like a Christmas present that would sneak up and pounce when I wasn’t looking. Much like Hobbes the tiger. It was awesome.

If the developer in your life doesn’t have any kind of wishlist, I would say the single greatest tool I use in a normal programming-intensive workday is a really good set of noise-canceling headphones. There are few things better than being able to fall into the coding zone with great tunes, and very legitimately not be disturbed by someone walking over to your desk and interrupting you. You won’t be able to hear them! :-)

Other posts