Arrays of Objects and __get: Friends Forever
In PHP, an object is always passed around as a reference, which allows one to deal with objects in a very transparent manner, since the only way to deal with a by-value copy instead of the real deal is to explicitly use the clone operator. Recently, I came upon a situation in which it was very useful for me to have an array of objects inside an object; the scenario was somewhat simple, a parent object can contain an indefinite number of children, and in order to have easy access to them I created a lazy loading property to contain them all as an array, indexed by their unique IDs. Of course, setting the stage for that is a bit more complicated than is needed for this example, so here is an extremely minimal example:
class foo { private $bar = array(); public function __construct() { $this->bar[0] = new stdClass; } public function __get($n) { return $this->bar; } }
So now we have a simple object with an array whose single element is an instance of PHP’s default object, stdClass. In reality you’d likely have more than just one element to the array, but it’s not necessary here to prove the point. Now, since objects are always returned by reference, accessing the first index of the array returned by __get when you try to access any member will allow you unfettered access to the contents of the object, to do with what you will (or rather, what the object will allow you to do).
With that in mind, let’s examine this:
$foo = new foo; $foo->bar[0]->baz = 'I am a test';
This code is pretty easy to follow, and in fact does exactly what you’d expect: the stdClass object sitting in the first element of the “bar” array has a new member, “baz”, defined and assigned. Viewing the contents of the object will show that this is exactly what happened:
["bar":"foo":private]=> { array(1) { [0]=> object(stdClass)#2 (1) { ["baz"]=> string(11) "I am a test" } } }
However, there’s a problem. Somewhere along the line, we generated a notice:
Notice: Indirect modification of overloaded property foo::$bar has no effect in …
While the notice certainly won’t halt the script’s execution, and the expected (and desired) action has taken place with no other apparent side effects, we are left with the conundrum of what to do with this notice (Note: While this issue has been brought to the attention of the PHP team, no word of a fix has yet surfaced). Since I am a firm believer that Notices and Warnings are potentially more dangerous than Fatal Errors, I won’t simply turn off error reporting; indeed, since the errors are still raised that doesn’t completely fix the small performance hit of generating the error, either.
In order to address this issue, it is important to understand what the notice is trying to tell us. Once upon a time, __get was a return-by-reference function by default. Of course, this doesn’t really help with wanting to prevent the modification of an object’s internal data, so __get was corrected to always return by value; in fact, even objects are “returned by value” in this case, since the value of the member variable is being returned (which just happens to also be a reference to an object), whereas the old __get would have returned a reference to the member variable itself; while the difference may seem subtle, it is monumental. Since this change occurred, it was important to notify coders that if they attempted to modify the contents of an array element which came from an overloaded array, this action would have no effect, as the modified element would only exist in the copy returned from __get.
Armed with this knowledge of history, we have a few obvious options for solving this problem
- public function & __get($n). This will technically prevent the warning from coming up, but if you’re going to go this route you might as well just declare all your member variables as public anyway, as this is what it will effectively cause __get to do. It opens the door to such dangerous situations as:
$foo->bar = 3;
That’s right, if you return by reference explicitly in __get, then you will circumvent any rules you’ve set for assignment via __set. Even objects are not immune to this, as a reference to the member variable (itself containing a reference) will be returned. This option removes the efficacy of even having visibility operators for anything you intend to provide overloaded access to.
- Assign a variable to the contents of the array element. Again, technically, this works, but it is messy, inelegant, and is nowhere near the ideal. Here are two examples:
$bar = $foo->bar; $bar[0]->baz = 'This works'; ### $bar = $foo->bar[0]; $bar->baz = 'This also works';
Again, though, this is not the clean, simple approach we were looking for to begin with.
- Just turn off notices. Nah, we ain’t doin’ that.
So what’s left to consider? After thinking about the problem for a little while, I realized that this problem wouldn’t even exist if I could just store the array as an object instead, but objects don’t allow numerical indices, so it would take a little jimmy-rigging to get it to work. Here was the first version:
class arrayReference { private $_ = array(); public function __set($n, $v) { $this->_[$n] = $v; } public function __get($n) { if (array_key_exists($n, $this->_)) { return $this->_[$n]; } $this->_[$n] = null; return $this->_[$n]; } public function __call($n, $a) { if ($n == 'array') { return $this->_(); } } public function _() { return $this->_; } } class foo { private $bar = null; public function __construct() { $this->bar = new arrayReference; $this->bar->{0} = new stdClass; } public function __get($n) { return $this->bar; } } $foo = new foo; $foo->bar->{1} = $foo; $foo->bar->{99} = new stdClass; $foo->bar->{99}->baz = 33;
Which, for the adjusted syntax, actually worked out pretty well. It might take more than an instant glance from your average PHP coder for what’s going on to make sense, or even seem syntactically correct, but it certainly worked; it even allowed for loop-based iteration by doing something like so:
foreach($foo->bar->array() as $k => $v)
While that isn’t ideal, it’s fairly transparent about what it’s doing.
I wish there were a more climactic way to put this, but there isn’t: The next step involved me trying to combine the SPL’s ArrayObject built in class to allow natural array access to my wrapper class, and after a few minutes playing with my new hideous child-beast amalgamate and its Reflection, I finally settled on this for the final version of the class:
class foo { private $bar = null; public function __construct() { $this->bar = new arrayObject; } public function __get($n) { return $this->bar; } }
No more messy syntax, no compromises, no hideous amalgamate beasts, and no figuring out how to mangle my behemoth class this lesson actually needed to be applied to in order to extend ArrayObject for the purposes of accessing just one property, as I saw advocated elsewhere during the googleing portion of my problem solving routine. The example I first gave? Works just fine, and no error since the property being returned is an object, not an array. Sometimes the best solution is fiendishly simple; the only real consideration I had to make here was that, in its actual application, the array in question was declared null so it could be lazy loaded, and since you can’t use the “new” keyword or even type-hinting in class member declarations, I had to be careful to make sure the lazy loading mechanism would still work, but I was never declaring a traditional array either: all in all, a 5 minute job to implement and test.
Five minutes that made the past day or so of work seem rather silly indeed.
Recent Entries
- Apache Taught You The Wrong Way To Think About Web Applications
- Adventures in Parsing: PHP’s implicit semicolon (‘;’) before every close tag
- The importance of ZVals and Circular References
- PHP Quirks – String manipulation by offset
- Let’s talk about your password model
- Pour Some Syntactic Sugar On Me: ‘Unless’ Keyword
- Arrays of Objects and __get: Friends Forever
- Did You Know? Class Visibility in PHP
- On Net Neutrality – A Plea
- Blankets – Nature’s Simple Truths