Just listen to Alex

July 21, 2011

PHP: Using objects as keys for a hash

Filed under: Uncategorized — bosmeeuw @ 1:58 pm

Hashes, or “associative arrays” are the bread and butter of PHP. Pretty much every PHP project will contain arrays this:

$peopleById = array(1234 => 'Alex', 1235 => 'Jimmy');

If you’re using PHP in an object oriented way, you might want to use hashmaps of objects, and you might want to use your objects as keys. So you try something like this:

class Author {
    public $name;

    function __construct($name) {
        $this->name = $name;
    }
}

class Book {
    public $name;

    function __construct($name) {
        $this->name = $name;
    }
}

$booksByAuthor = array();

$author = new Author("Ian McEwan");

$booksByAuthor[$author] = array(new Book("Atonement"), new Book("Solar"));

However, PHP will throw a “Warning: Illegal offset type” when trying to use the Author object as a key, and your array will be empty.

Fear not, we can fix this, with our very own hashmap class! Only one change to the code is needed:

$booksByAuthor = new HashMap();

$author = new Author("Ian McEwan");

$booksByAuthor[$author] = array(new Book("Atonement"), new Book("Solar"));

Unfortunately, it probably isn’t possible (or I haven’t found out how) to use a standard foreach($hash as $key => $value) to loop over this HashMap, but we can use this form:

foreach($booksByAuthor->keys() as $author) {
    foreach($booksByAuthor[$author] as $book) {
        echo $author->name . " wrote " . $book->name . "<br />";
    }
}

You can use $booksByAuthor->values() to loop over the values.

By default, this hashmap uses object identity to keep the keys apart. This means that if you create two objects, they will represent a different key, even if their values are exactly the same. For example, the assertion below will fail:

class Customer {
    public $id;

    function __construct($id) {
        $this->id = $id;
    }
}

$customer = new Customer(123);
$sameCustomer = new Customer(123);

$creditPerCustomer = new HashMap();

$creditPerCustomer[$customer] = 1000;
$creditPerCustomer[$sameCustomer] = 1500;

assert($creditPerCustomer[$customer] == 1500);

Because “sameCustomer” is a different object, the value will remain at 1000 and the assertion will fail, even though we might want to treat it as an equivalent key. We can fix this by having Customer implement interface HashCodeProvider, like this:

class Customer implements HashCodeProvider {
    public $id;

    function __construct($id) {
        $this->id = $id;
    }

    public function getHashCode() {
        return $this->id;
    }
}

$customer = new Customer(123);
$sameCustomer = new Customer(123);

$creditPerCustomer = new HashMap();

$creditPerCustomer[$customer] = 1000;
$creditPerCustomer[$sameCustomer] = 1500;

assert($creditPerCustomer[$customer] == 1500);

Your getHashCode() method can return any scalar value (int, string, etc), as long as it really represents the identity of the object.

You can grab the simple code for the HashMap class below. Happy hashing!

interface HashCodeProvider {
    public function getHashCode();
}

class HashMap implements ArrayAccess {

    private $keys = array();

    private $values = array();

    public function __construct($values = array()) {
        foreach($values as $key => $value) {
            $this[$key] = $value;
        }
    }

    public function offsetExists($offset) {
        $hash = $this->getHashCode($offset);

        return isset($this->values[$hash]);
    }

    public function offsetGet($offset) {
        $hash = $this->getHashCode($offset);

        return $this->values[$hash];
    }

    public function offsetSet($offset, $value) {
        $hash = $this->getHashCode($offset);

        $this->keys[$hash] = $offset;
        $this->values[$hash] = $value;
    }

    public function offsetUnset($offset) {
        $hash = $this->getHashCode($offset);

        unset($this->keys[$hash]);
        unset($this->values[$hash]);
    }

    public function keys() {
        return array_values($this->keys);
    }

    public function values() {
        return array_values($this->values);
    }

    private function getHashCode($object) {
        if(is_object($object)) {
            if($object instanceof HashCodeProvider) {
                return $object->getHashCode();
            }
            else {
                return spl_object_hash($object);
            }
        }
        else {
            return $object;
        }
    }

}

Blog at WordPress.com.