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 foreach
ing 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
, andvalid
from theTraversable
interface.getIterator
from theIteratorAggregate
interface.offsetExists
,offsetGet
,offsetSet
, andoffsetUnset
from theArrayAccess
interface.count
from theCountable
interface.serialize
andunserialize
from theSerializable
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! :-)