Josh

I'm a developer in Melbourne, Australia, and co-founder of Hello Code.

Published Thu 01 January 2009

← Home

Adventures in PHP interfaces

So recently I've been having a play with Python. I like it a lot, and it's started to affect how I code in PHP — all of my freelance work still uses PHP, so it's still my 'primary' coding language. However, this means that all the little things I can do faster in Python come back to haunt me in PHP. It was bound to happen. Unfortunately, though, I can't just switch all my work to Python (and it has a number of shortcomings that make it harder to support, anyway) so to resolve this I've been attempting to replicate, in my PHP framework Rex, some of the things which in Python make my life easier.

The first of these is the syntactic sugar of SQLAlchemy's (and AppEngine's, Django's, and others) data selection syntax. With some nifty method chaining, SQL queries can be abstracted to such pretty code (yes, in PHP) as $user = User->all()->filter('Admin = 0')->order('FirstName','ASC')->go()->get(0);. I might follow up this post with another explaining how to achieve this method chaining, and it's really quite easy, but in the meantime I want to draw your attention to something else.

Scrutinise the line of code above for a moment. The very last function call is get(0), which will return the first index in the results array. Hold on, an array? That's right. Usually to get an array of rows back, there is a two-stage process in accessing the first index:

$results = $User->someMethodReturningUsers();

$user = $results[0];

It always really bothered me that you couldn't just chain an array index to the end, like $user = $User->someMethodReturningUsers()[0]; which is perfectly acceptable in a lot of other dynamic languages. But, aha! That's what we're about to solve.

ArrayAccess, Countable, and Iterator interfaces

As of PHP 5, the SPL has some very interesting (and largely obscure) classes and interfaces hidden away. The three I've listed here allow any class that implements them to act as an array.

Why would you want to do this? Well, for example, let's say that your random someMethodReturningUsers() now returns your own class AwesomeArray. Does the end-user need to know that? Not really. If they like, they can continue on oblivious and execute our two-step plan above. Everything's peachy. But, but... you now know better. Because AwesomeArray not only implements those array-like interfaces, it has a get() method that takes an integer and returns that array index.

Bam! Your process now looks like $user = $User->someMethodReturningUsers()->get(0);. But that's not all, you get the best of both worlds. You can have you cake and eat it too, because as well as chaining methods as shown, you can still use the AwesomeArray returned as you would a regular array. Iterate over it using foreach() (that's what the Iterator interface handles), access its indexes (ArrayAccess), and check its length via count() (Countable).

I'm not sure that this is a perfect solution (there may be array methods like array_unique that just don't work) but when used in conjunction with some sugary syntactic goodness, it really is the cherry on top, the finishing touch.

I've included the code below so you can roll your own. Enjoy!

class Result implements ArrayAccess, Countable, Iterator {

private $position = 0;
private $data = array();

public function offsetSet($offset, $value) {

    if ($offset == ''){
        $this->data[] = $value;
    } else {
        $this->data[$offset] = $value;
    }
}
public function offsetExists($offset) {

    return isset($this->data[$offset]);
}
public function offsetUnset($offset) {

    unset($this->data[$offset]);
}
public function offsetGet($offset) {

    return $this->data[$offset];        
}

public function get($index){
    return $this->data[$index];
}

public function count(){
    return count($this->data);
}

public function rewind(){
    reset($this->data);
}

public function current(){
    return current($this->data);
}

public function key(){
    return key($this->data);
}

public function next(){
    return next($this->data);
}

public function valid(){
    return $this->current() !== false;
}

public function size(){
    return count($this->data);
}

}

To top