If you’re at home with languages like Perl or Javascript you may be used to treating objects and arrays as variations on a theme. PHP is traditionally stricter than that. You can create or change an element directly on an array with assignment, but to set a new property on an object you generally need to create a setter method – or open up a property for access. With the ArrayAccess
interface you can get the best of both worlds. It allows client code to access a PHP object like an array.
In other words you can add and retrieve values as if you’re working with an associative array but with the encapsulated functionality of an object.
Note Whoah – jargon check. What did I mean by encapsulated functionality? In fact this is one of the coolest things about object-oriented coding – it simply means that any dirty logic, like the processing or checking of values, is hidden away on the inside behind a nice clean interface. There are two key benefits to this. Firstly, when you place logic on the inside, your client code gets cleaner. It makes a simple call and any messy or complicated business happens elsewhere. Secondly, and probably more importantly, putting logic in a class reduces duplication. You manage your complicated business in one place and, if you change it, you only need do so once.
In practical terms, accessing a PHP object like an array means that you have the convenience of an array but with the availability of additional processing of the data on the way in or out of the object. So how do we make this happen? It’s really only a matter of implementing a built in interface named ArrayAccess
.
A test project
If you use Silex you may have come across Pimple, the cool but horribly-named dependency injection container which uses this technique to great effect. In its honour I’m going to create a much more basic class named Callbacky
. Its job? To store callbacks as if they were values. These will be transparently invoked at access time. Here’s how you might use Callbacky
:
$c = new Callbacky();
$c['sayhello'] = function($c) {
return "hello you!";
};
$c['storemynumber'] = 5;
print $c['storemynumber']; // 5
print $c['sayhello']; // hello you!
So, even though the $c
variable contains an object, I am able to assign an integer value to it as if it’s an array. I can also assign an anonymous function to it. What’s more, when I access that element later, instead of getting the function back I get the result of having run it. Obviously, in a real usage, the callback would likely perform some more involved or expensive function. We might also allow for the configuration of callbacks – setting some to be run only once, for example. (Interested in that? I added it on at the end as a bonus). Let’s press on with the simplest brief though.
Declaring the ArrayAccess class
That’s just a matter of implementing an interface:
namespace getinstance\arrays;
class Callbacky implements \ArrayAccess
{
// ...
}
By implementing ArrayAccess
I’m committing to implement certain methods. Here are their signatures:
public function offsetSet($key, $value)
public function offsetExists($key)
public function offsetUnset($key)
public function offsetGet($key)
offsetSet()
will be called when a key and value are set – either like this:
$c['mykey'] = "myval";
Or like this
$c[] = "someval";
In this case, offsetSet()
will be called with the $key
argument set to null
.
offsetExists()
will be called when the object is tested like this:
isset($c['sayhello']);
offsetUnset()
will be called when an element is removed:
unset($c['sayhello']);
offsetGet()
will be called when an element is accessed
print $c['sayhello'];
Armed with that information, let’s go ahead and implement Callbacky
.
Assigning to a PHP object like an array: the offsetSet() method
I will need a property in which to store the values passed to offsetSet()
. Then it’s just a matter of applying the provided key and value.
class Callbacky implements \ArrayAccess
{
private $storage = [];
public function offsetSet($key, $value)
{
if (is_null($key)) {
$this->storage[] = $value;
} else {
$this->storage[$key] = $value;
}
}
// ...
}
Remember that $key
will be null (for an assignment like $myobj[] = 4
) – in which case I just add the provided value to the array. If the key is provided, I use it as the array key. And that’s all there is to it!
Checking and removing elements: The offsetExists() and offsetUnset() methods
These are pretty simple. In each case a key is provided – and it’s just a matter of either testing or unsetting the element on the $storaage
array property.
public function offsetExists($key)
{
return isset($this->storage[$key]);
}
public function offsetUnset($key)
{
unset($this->storage[$key]);
}
That will allow me to treat a Callbacky
PHP object like an array when it comes to checking and removing elements.
if (isset($c['sayhello'])) {
//...
}
unset($c['sayhello']);
Query a PHP object like an array: the offsetGet() method
This is our opportunity to make some magic happen. Remember, the point of Callbacky
is to return either a straight value or the result of invoking a callback for an element.
public function offsetGet($key)
{
if (! isset($this->storage[$key])) {
return null;
}
$val = $this->storage[$key];
if (is_callable($val)) {
return $val($this);
}
return $val;
}
The method is given a $key
. If the element does not exist, I return null
. If the element contains callable code, I invoke it and return the result. Otherwise, I just return the value. Note that offsetGet()
passes an instance of itself to the stored function. This isn’t that useful in this example – but if Callbacky
were to have more functionality or contextual information it could be something a callback function would want access to.
Bonus: extending Callbacky
You don’t need this section to work with ArrayAccess
– it’s just for fun.
So, where next for Callbacky
? Well, one nice method would be distinguish between callbacks that should run just once and those that should run every time an element is queried. We can make use of the Callbacky
class’s own features to make this work:
public function once($key, $val)
{
if (is_callable($val)) {
$this[$key] = function ($c) use ($key, $val) {
$ret = $val($c);
$c[$key] = $ret;
return $ret;
};
} else {
$this[$key] = $val;
}
}
If a callback is added through once()
– like this one:
$c->once("expensive", function($c){
return "an expensive operation!";
});
then it is itself wrapped in another callback. This outer callback invokes the first when it is called but then sets the return value on the storage array – thereby ensuring that the value is returned for subsequent calls and that the inner callback is never invoked again.
_
Apple and oranges. by jollyjoeroger
is licensed under CC BY 2.0_