Author: xdissent January 15th 2010 6:55 pm
Keywords: descriptors, OOP
So you’ve been working in Python for a year and then along comes a PHP gig that you just can’t pass up. All of a sudden you’re realizing just how much you miss some of the familiar Pythonisms you’ve come to rely on. One such feature that is lacking in PHP is the concept of descriptors. Fortunately, it’s possible to pretty closely simulate their behaviour in PHP with just a few special interfaces and classes.
Mind Blowing
For the uninitiated, there are a few great articles online that go into great detail about what Python descriptors are, and what they can do. Descriptors are a notoriously mind boggling topic that has left many a curious developer cross-eyed and drooling, and usually more confused than we he started. But they’re simple to understand, really: They’re just object instances posing as dynamic properties which are defined on a class, evaluated at runtime by a method on the descriptor object itself, that are accessible through either an object instance, or statically through a property on the the class itself!
Yeah. "Simple."
To be honest, most of the difficulty in understanding descriptors stems from the fact that (especially in PHP,) classes aren’t really thought of as "objects" much. Classes in general are a little bit "quirky" in PHP, being referenced as a quoted class name string half the time, an unquoted global class name the other half, and as a special keyword the other-other half! Only now are the more advanced class related tools even appearing in the PHP world, and they’re still not complete (which will really screw us later in this article). Plus there’s just not that much information about down and dirty class-level manipulation for PHP out there. Where the official documentation is lacking, it’s only supplemented by helpless user comments flailing wildly trying to understand the murky examples. It’s a real shame because ultimately, we as developers fall victim to the catch 22 of PHP’s evolution as a language: Features will be added after widespread adoption of the feature." Progress happens very slowly because developers lack the understanding (or perhaps the vision) to take advantage of some of these features – much less successfully lobby for improvements in the language itself. Regardless, I’ll admit I never really even thought of classes in such a dynamic way until I switched to Python. So, if you’ve never been exposed to Python and its very object-like class syntax, then it’s going to be an even longer road to enlightenment! No worries though, let’s just break it down slowly and see what descriptors really look like: Descriptors are…
object instances…
Ok, so that means we’re dealing with an instance of a class, which will obviously require a class definition. That’s a pretty simple concept – define a class and create an instance of it. Somewhere along the line, we’re going to have to define some kind of "descriptor" class, and instantiate it. No big deal. With that in mind, let’s move on: Descriptors are object instances…
posing as dynamic properties…
Dynamic properties are a slightly advanced concept, and usually make use of the __get magic method (and friends), which PHP automatically calls when you try to access a non-existant property (member variable) on an object instance. For example, in the following code, $obj will be an instance of TheClass and the_property will be normal, non-dynamic instance property:
<?php
// Define the class.
class TheClass
{
// Declare the property.
public $the_property;
}
// Create the object instance.
$obj = new TheClass();
// Access the object instance's property.
echo 'The property: ' . $obj->the_property;
The property is accessed as $obj->the_property. The following call will issue a Notice level error by default, since we attempt to access an undeclared property on $obj:
<?php
// Access a nonexistent property.
echo $obj->bogus_property;
However, if the class defines an instance method named __get, it can intercept requests for undefined properties, and return a dynamic value. That means we can calculate a value at runtime for what the world will think is a normal instance property. The end product looks something like this:
<?php
// Define the class.
class DynamicClass
{
/**
* Returns the value of a dynamic property.
*
* This method will issue a 'Notice' level error if a nonexistent
* property is requested.
*
* @param string $name The name of the property to get.
*
* @return mixed
*/
public function __get($name)
{
// Check the name of the requested property.
if ($name == 'the_property') {
// Return the dynamic value for 'the_property' property.
return time();
}
// Trigger an error if no property was found with the name.
trigger_error(sprintf(
'Undefined property: %s::$%s',
get_class($this),
$name
));
}
}
// Create the object instance.
$obj = new DynamicClass();
// Access the object instance's property.
echo 'The property: ' . $obj->the_property;
Now every time we access the $obj->the_property property, the __get method will be evaluated, and – specifically – the current time will be returned.
That’s really all there is to the concept of dynamic properties. They’re incredibly convenient though, and indispensable when you want to define a clean and easy to understand API or standard interface to some functionality. There are in fact even more magic methods, allowing you to control property access in contexts other than "getting." We’ll deal with a few of them later.
Let’s get back to unraveling the mystery of descriptors. We know that they’re object instances (duh), and that they themselves somehow pose as dynamic properties. That means that, similarly to the __get method we’ve just seen, descriptors handle access requests for undefined properties. It’s not clear how an object instance actually does this amazing feat yet, but let’s take it one step at a time: Descriptors are object instances posing as dynamic properties…
which are defined on a class…
Holy crap, say what? Defined on a class? How do you define a property on a class? Well, in the biz, we call a property defined on a class, a static property. "Class members", "class properties", "static properties" – it’s all the same. In fact, the PHP documentation can’t even make up its mind what to call them. The premise is simple though. Remember that in PHP, classes are defined globally. It might help to think of the class definition itself as just a block of sourcecode that creates a single global instance of a "class" type object the first time it is run. These global "class" object instances have their own properties and methods, plus they know the how to do the voodoo (that they do) to create a local instance of the class they represent (so well). To declare a static class property, you just use the static keyword in the class definition, and that property will now only be accessible through the class itself and not through instances. The scope resolution operator can be used to get the static property’s value:
<?php
class StaticPropertyClass
{
// Declare the static property.
public static $the_static_property;
// Declare the property.
public $the_property;
}
// Create the object instance.
$obj = new StaticPropertyClass();
// Access the object instance's property.
echo 'The property: ' . $obj->the_property;
// Access the static class property.
echo 'The class property: ' . StaticPropertyClass::$the_static_property;
// This will produce an error.
echo $obj->the_static_property;
// And so will this.
echo StaticPropertyClass::$the_property;
As you can see, class properties are completely separate from (and oblivious to) instance properties. They represent a unique global value, associated with the theoretical "global instance" of the "class" type object. Got it? Good. To review, a descriptor is really just an object that happens to be stored in a class property on some random class. It also will intercept and specially handle requests for some sort of dynamic property, which we haven’t really talked about yet. We do know that the value for the hypothetical property will be…
evaluated at runtime by a method on the descriptor object itself…
That means that every time we try to get the value of the dynamic property, the descriptor instance will run an instance method to determine the returned value. So a descriptor has to define some kind of processing method, which is loosely equivalent to the __get method we used earlier. The real difference is that the PHP magic methods only automatically handle access to dynamic properties on the exact instance on which the property is accessed. The descriptor differs in that (somehow) it intercepts property access for a different object, not itself.
Now our picture of a descriptor is complete, even though we don’t really know how it’s going to work or what it’s for yet. According to what we just discussed, we’re simply dealing with an instance of a class, with a method that will calculate and return the dynamic value of the property which the descriptor instance represents. And it goes a little something like this:
<?php
// Define the descriptor class.
class DescriptorClass
{
/**
* Calculates and returns the dynamic value for the property
* associated with the descriptor instance.
*
* @return mixed
*/
public function getDescriptor()
{
// Do some special calculations.
return time();
}
}
// Create a descriptor.
$the_descriptor = new DescriptorClass();
// Add the descriptor to the class.
StaticPropertyClass::$the_static_property = $the_descriptor;
That’s it! Now $the_descriptor object fits all of the criteria of a descriptor so far. It’s stored in a static class property (StaticPropertyClass::$the_static_property), it’s an instance of a class and it has an instance method called getDescriptor that will return a dynamic value for some property somewhere. Ah yes – we have to figure out how the descriptor relates to the property. Recall that descriptors intercept requests for dynamic properties…
that are accessible through either an object instance…
Hold it right there! Forget the "either" for right now and let’s focus on this part. The dynamic properties we’re going to be using with descriptors are accessible through an object instance. So really, when we’ve got our descriptor working correctly, we should be able to access a dynamic instance property just like we did using __get earlier. The important thing to note is that the name of the static class property in which the descriptor is stored should be the name of the instance property we use to get the dynamic value from the descriptor instance. I’ll say that again – store a descriptor instance in a class property, use the name of that class property to access the descriptor in an instance of that class. For example, using the descriptor we stored in StaticPropertyClass::$the_static_property, we would access the descriptor’s calculated property value using $obj->the_static_property:
<?php
// Create the object instance.
$obj = new StaticPropertyClass();
// Access the dynamic property, calculated by the descriptor instance.
echo 'Current time: ' . $obj->the_static_property;
Every time we access $obj->the_static_property, we need StaticPropertyClass::$the_static_property->getDescriptor() to run. Now that we’re clear, we can actually go about implementing some interfaces and a class that will take care of the actual property request handling. It’s not as hard as it sounds! Let’s start by defining a DescriptorInterface interface, which will need to be implemented by each descriptor class we create.
<?php
// An interface that all descriptors will implement.
interface DescriptorInterface
{
/**
* Returns the value of the descriptor when accessed.
*
* @param object $instance The object instance on which the descriptor was
* accessed. 'null' will be passed if accessed the
* descriptor was accessed statically.
* @param string $owner The name of the class which owns the descriptor.
*
* @return mixed
*/
public function getDescriptor($instance, $owner);
}
Notice that we added parameters to the getDescriptor method. These are the parameters specified by the Python descriptor interface and they’ll really come in handy later. For now just understand that the first parameter is always the object instance whose descriptor property is being accessed and the second parameter will always be the name of the class on which the descriptor is defined. For example:
<?php
// Define a descriptor.
class ChattyDescriptor implements DescriptorInterface
{
// Calculate the dynamic value.
public function getDescriptor($instance, $owner)
{
return sprintf(
"Hey %s, you're a %s.",
$instance->name,
$owner
);
}
}
// Define a simple class.
class Dude
{
public $name;
public static $descriptor;
// Assign the name at creation.
public function __construct($name)
{
$this->name = $name;
}
}
// Add the descriptor instance to the class.
Dude::$descriptor = new ChattyDescriptor();
// Create an object instance.
$obj = new Dude('Broseph');
// Access the descriptor.
echo $obj->descriptor;
The previous example will output Hey Broseph, You're a Dude. if everything goes according to plan – which it won’t, until we sprinkle some magic code dust on the Dude class. What we need Dude to take care of is the hand-off from the dynamic property request to the descriptor’s getDescriptor method. Since we’re definitely not going to want to add extra code to every single class we use a descriptor with, let’s agree that we’ll need a single base class from which all descriptor-bearing classes will be extended. In this class, we want to intercept requests for undefined instance properties, check for a descriptor with the same name in the static class properties of the instance’s class, and then call the class property’s getDescriptor method if we do in fact find a descriptor object. Here’s one implementation of just such a base class:
<?php
// Define a base class for objects that will use descriptors.
abstract class Descriptable
{
// Intercepts requests for nonexistent properties.
public function __get($name)
{
// Get the name of this class.
$class = get_class($this);
// Get an introspection reflection for the class.
$class_ref = new ReflectionClass($class);
// Check for a static class property with the given name.
$attr = $class_ref->getStaticPropertyValue($name, null);
// If the class property is an object, check for a descriptor.
if (is_object($attr)) {
// Get an introspection reflection for the property.
$attr_ref = new ReflectionClass(get_class($attr));
// Check to see if the static property is a descriptor.
if ($attr_ref->implementsInterface('DescriptorGet')) {
// Call the descriptor method and return the value.
return $attr->getDescriptor($this, $class);
}
}
// Trigger an error if no descriptor was found.
trigger_error(sprintf('Undefined property: %s::$%s', $class, $name));
}
}
Subclasses of Descriptable will now correctly handle descriptors with no further fiddling. PHP’s new reflection API (speaking of undocumented features) is used to inspect the subclass and the descriptor instance to determine whether descriptor handling should take place. Here’s a final, working example that will correctly implement a descriptor according to the Python rules we’ve been over so far:
<?php
// Define a simple class.
class WorkingDude extends Descriptable
{
public $name;
public static $descriptor;
// Assign the name at creation.
public function __construct($name)
{
$this->name = $name;
}
}
// Add the descriptor instance to the class.
WorkingDude::$descriptor = new ChattyDescriptor();
// Create an object instance.
$obj = new WorkingDude('Brosephus');
// Access the descriptor (works!)
echo $obj->descriptor;
Brilliant! We’ve made a descriptor! The only thing we need to remember about using the Descriptable class is that if we override the __get method, we must call the __get method inherited from Descriptable. When we call it in the overridden method will essentially effect the resolution order of dynamic properties, so do tread lightly in these cases.
So far we’ve built a descriptor class that emulates Python’s descriptors in all cases but one. Python descriptor properties may be accessed either through an object instance (as we’ve just shown)…
or statically through a property on the the class itself!
And here’s where we hit the proverbial brick wall. Basically what we need is a way to trigger a descriptor’s dynamic processing when accessing a static class property. The really confusing part is that generally, the static property will actually contain the descriptor instance itself! Mind Blow part II, anyone? So every time we access a static class property that contains a descriptor instance, we want to receive the dynamic value of the descriptor, not the descriptor instance itself. If we could get that to work, it would look like this:
<?php
// Define a simple class.
class UnemployedDude extends Descriptable
{
public $name;
public static $descriptor;
// Assign the name at creation.
public function __construct($name)
{
$this->name = $name;
}
}
// Add the descriptor instance to the class.
UnemployedDude::$descriptor = new ChattyDescriptor();
// Access the descriptor on the class (doesn't work!)
echo UnemployedDude::$descriptor . " You don't even exist, you lazy bum!";
Unfortunately, PHP has a problem. It provides no __get equivalent magic method for static access. Try as we might, we’ll never get a dynamic value for a class property (defined or otherwise). There has been great discussion, a legendary bug ticket and even an RFC dealing with the addition of a __getStatic magic method to the PHP core, but PHP 5.3 shipped without even a hint of progress in this area. Quel bummer, man! So all static class properties must be declared in the class definition, period. In fact, our descriptor implementation subtley relies on this quirk. When __getStatic is finally available our descriptor class will require that the class property be undefined in the class definition, and assigned later. The assignment would take place in the __setStatic magic method, which would be tasked with keeping track of added descriptors, most likely in an array keyed off the property name for each descriptor. Yes, it will be a brave new world for sure! Oh well, it looks like we were just wasting our time on this descriptor pipe dream.
Not so fast! Don’t throw your keyboard in the trashcan yet – there is hope. First, let’s examine precisely how big of a deal this one restriction on our PHP descriptors is. Why would you even want static access to a dynamic descriptor property? Coincidentally, you probably wouldn’t want access to the dynamic value at all! In practice, descriptors rarely define special handling for the class itself, rather they focus on manipulating object instances. So the use cases are few in which you will even be dealing with a descriptor value statically. What you’ll see far more often is a descriptor returning its own instance instead of a value when it is accessed as a static class property. If you think about it, this makes total sense. How else are you supposed to ever get at the descriptor instance otherwise? If UnemployedDude::$descriptor returned a value, there would be no way to get at the descriptor instance at all, since that’s the only way we know how to refer to the damn thing. This is just how PHP works (which we’re stuck with) and it happens to correlate with the most likely use case for a descriptor (luckily) so our descriptor class is still very faithful to the Python equivalent.
One quick thing to note is that the dynamic accessor method required by the descriptor interface accepts an instance parameter which will contain the object instance whose descriptor property was requested. In a class descriptor context, this parameter would always be null since there is no instance in a static context.
Meet the Family
Up to this point, we’ve ignored a huge detail. In reality, "getting" isn’t the only access method supported by Python descriptors. There is also support for dynamic value "setting" and "deleting." These actions roughly correspond to the PHP magic methods __set and __unset, and should be fairly self explanatory. Descriptors intercept property "setting" and "unsetting" just as they do for property "getting" currently. To fill out our descriptor implementation, we should define some new interfaces to define how these new methods will be called by the descriptable class.
<?php
// The descriptor interface for "set" access.
interface DescriptorSet
{
/**
* Sets the value of the descriptor.
*
* @param object $instance The object instance on which the descriptor was
* accessed.
* @param mixed $value The value that was given to the descriptor.
*
* @return null
*/
public function setDescriptor($instance, $value);
}
// The descriptor interface for "unset" access.
interface DescriptorUnset
{
/**
* Unsets the descriptor's value.
*
* @param object $instance The object instance on which the descriptor was
* accessed.
*
* @return null
*/
public function unsetDescriptor($instance);
}
The setDescriptor method will be called when setting the value of a descriptor property like $obj->descriptor = 'new value'; and will receive the new property value in the $value parameter. Note that the unsetDescriptor method only receives the object instance as a parameter. If you’re wondering what happened to the $class parameter that we used in getDescriptor, good catch! It turns out, Python descriptors do not allow static access for "setting" and "deleting (unsetting)" descriptor properties. The reasoning is simple: if these methods were allowed for static property access, you could never remove the descriptor instance from the class. A full restart of the program would be required to empty the static class property. That just isn’t practical, so the Python authors left the feature out completely to prevent confusion. This is an extra bonus for us, since we can’t use static class property access at all with descriptors in PHP. That means we’re really only missing out on the one single use-case.
There is one other detail to descriptors that’s specific to PHP. Many PHP developers use isset() to evaluate whether a property exists and is valid. Unfortunately isset() will return false for any undeclared property, even if we’ve overridden the __get method to return a value. To accurately simulate a real property, we need to override the magic __isset method to return true if a property is evaluated dynamically. With this last piece of the puzzle, we are able to construct a robust descriptable class, completing our PHP descriptor support:
<?php
// The base descriptor interface.
interface Descriptor
{
}
// The descriptor interface for "get" access.
interface DescriptorGet extends Descriptor
{
/**
* Returns the value of the descriptor when accessed.
*
* @param object $instance The object instance on which the descriptor was
* accessed. 'null' will be passed if accessed the
* descriptor was accessed statically.
* @param string $owner The name of the class which owns the descriptor.
*
* @return mixed
*/
public function getDescriptor($instance, $owner);
}
// The descriptor interface for "set" access.
interface DescriptorSet extends Descriptor
{
/**
* Sets the value of the descriptor.
*
* @param object $instance The object instance on which the descriptor was
* accessed.
* @param mixed $value The value that was given to the descriptor.
*
* @return null
*/
public function setDescriptor($instance, $value);
}
// The descriptor interface for "unset" access.
interface DescriptorUnset extends Descriptor
{
/**
* Unsets the descriptor's value.
*
* @param object $instance The object instance on which the descriptor was
* accessed.
*
* @return null
*/
public function unsetDescriptor($instance);
}
// The base class for all descriptor-bearing subclasses.
abstract class Descriptable
{
/**
* Finds and returns a descriptor instance for the class.
*
* This method will return null if either a descriptor was not found in
* the class property with the specified name, or if a descriptor was
* found but does not implement the requested interface. Passing the
* default interface name 'Descriptor' will return any type of descriptor
* as long as it is stored in the correct class property.
*
* @param string $name The name of the descriptor property being accessed.
* @param string $iface The name of the descriptor interface which must
* be supported by the descriptor instance.
*
* @return mixed
*/
protected function _descriptorInstance($name, $iface='Descriptor')
{
// Get an introspection reflection for the class.
$class = get_class($this);
$class_ref = new ReflectionClass($class);
// Get the static class property with the given name.
$attr = $class_ref->getStaticPropertyValue($name, null);
// Check to see if the property is a descriptor instance.
if (is_object($attr)) {
// Get an introspection reflection of the property.
$attr_ref = new ReflectionClass(get_class($attr));
// Check to see if the static property has the right interface.
if ($attr_ref->implementsInterface($iface)) {
// Return the found descriptor.
return $attr;
}
}
// Return null since we didn't find a matching descriptor.
return null;
}
/**
* Finds and runs a descriptor method for the class.
*
* This method will run the specified descriptor method of the descriptor
* with the provided name if it exists. The method may be one of 'get',
* 'set', or 'unset'. Any arguments provided in the '$args' array parameter
* will be passed to the descriptor method. This method will return 'null'
* if no matching descriptor instance was found.
*
* @param string $method The name of the descriptor method to run.
* @param string $name The descriptor property name.
* @param array $args An array of arguments to pass to the descriptor
* method or 'null'.
*
* @return mixed
*/
protected function _descriptorAccess($method, $name, $args=null)
{
// Initialize descriptor method arguments.
if (is_null($args)) {
$args = array();
}
// Get the name of the required descriptor interface.
$iface = 'Descriptor' . ucfirst($method);
// Retrieve the descriptor instance matching the name and interface.
$attr = $this->_descriptorInstance($name, $iface);
// Check for a valid descriptor instance.
if (!is_null($attr)) {
// Call the descriptor instance method with the passed arguments.
return call_user_func_array(
array($attr, $method . 'Descriptor'),
$args
);
}
// Trigger an error if the appropriate descriptor wasn't found.
trigger_error(sprintf('Undefined property: %s::$%s', $class, $name));
return null;
}
/**
* Gets a dynamic instance property's value.
*
* @param string $name The name of the instance property being accessed.
*
* @return mixed
*/
public function __get($name)
{
$args = array($this, get_class($this));
return $this->_descriptorAccess('get', $name, $args);
}
/**
* Sets a dynamic instance property's value.
*
* @param string $name The name of the instance property being set.
* @param string $value The new value to use for the property.
*
* @return null
*/
public function __set($name, $value)
{
$args = array($this, $value);
return $this->_descriptorAccess('set', $name, $args);
}
/**
* Unsets a dynamic instance property.
*
* @param string $name The name of the instance property being unset.
*
* @return null
*/
public function __unset($name)
{
$args = array($this);
return $this->_descriptorAccess('unset', $name, $args);
}
/**
* Returns true if a descriptor instance exists in the named class property.
*
* @param string $name The name of the instance property being accessed.
*
* @return boolean
*/
public function __isset($name)
{
// Simply test to see if we have any descriptor with that name.
return is_null($this->_descriptorInstance($name));
}
}
The Gotchas
There are some tiny differences between our PHP descriptors and those in Python, aside from the static access restriction discussed previously. Python’s property resolution automatically searches for a class property if an instance does not contain property with the requested name. PHP does not do this, and treats static properties very differently. This is unlikely to become an issue in practice, and is a very specific edge case. Another similar detail is that Python will short circuit its property resolution if a property defines a "setting" access method, always using the descriptor even if the instance defines it’s own property with the same name. Descriptors which only handle "getting" of properties will not be used by default. This is very confusing and really just an intricacy of Python that doesn’t relate to PHP since we don’t have the concept of an object’s "data dict." In all except the wildest of edge cases, these differences may be ignored.
Another small gotcha can pop up when developers erroneously re-include a source file containing a class definition. Since descriptor instances must be assigned to a class property and PHP doesn’t allow evaluated variables as property values in the class definition, we’re forced to add the descriptor instance to the class after it has been defined. This isn’t something that should be feared and it’s certainly not "wrong" according to how PHP works, but it is a little off putting to some developers. Regardless, it’s important to not re-include a file if it adds a descriptor to a class. Otherwise, a new instance of the descriptor may suddenly appear in all of the existing instances of that class. The fix is simple: use require_once like you should be doing in the first place when importing class definitions.
The Payoff
At this point you’re probably very angry (and crosseyed) after having read this exhausting tome, and yet we still haven’t explored why descriptors are useful in the first place. Let’s go over a few benefits descriptors have over other types of dynamic properties.
Easy (Class-wide) Caching
A classic use case for dynamic properties is value caching. If the value of a property is expensive to calculate, it’s trivial to set up a dynamic property that calculates the value once, store it locally in the object instance and return the calculated value upon subsequent access. The down side to simple caching (probably using the __get method) is that the cache is local to the object. That means in naive implementations the value is calculated and stored each and every time you create an object instance and try to access the dynamic property. This is unneccessary when the calculated value will not differ between object instances. Of course this problem only gets larger the more instances you actually create.
Descriptors open up the possibility of per-class caching, rather than per-object caching since descriptor instances are defined on a class. Even when accessed on different object instances, a single descriptor property instance is handling all of the requests. This allows the descriptor to be used as a class-wide cache with very little effort. Using a descriptor in this way can be a lifesaver for intensive operations like database access:
<?php
// Define a descriptor to manage retrieving the high score.
class HighScoreDescriptor
implements DescriptorGet, DescriptorSet
{
protected $_high_score;
// Returns the current high score.
public function getDescriptor($instance, $owner)
{
// Check to see if we've already retreived the high score.
if (is_null($this->_high_score)) {
// Fetch and cache the high score from the db.
$this->_high_score = get_from_db('high_score');
}
// Return the cached high score.
return $this->_high_score;
}
// Sets the current high score.
public function setDescriptor($instance, $value)
{
// Update the cached high score.
$this->_high_score = $value;
// Save the new high score to the db.
update_db('high_score', $this->_high_score);
}
}
// Define a scored game class.
class ScoredGame extends Descriptable
{
// Define a class property to hold the high score descriptor.
public static $high_score;
}
// Add a high score descriptor to the class.
ScoredGame::$high_score = new HighScoreDescriptor();
// Create a game instance.
$game = new ScoredGame();
// Retrieve the high score for the game (db queried).
echo 'High Score: ' . $game->high_score;
// Set the high score, cheater.
$game->high_score = 31337;
// Retreive the high score again (db *not* queried).
echo 'New High Score: ' . $game->high_score;
// Create another game instance.
$another_game = new ScoredGame();
// Retrieve the high score for the new game (db *still* not queried).
echo 'Same High Score: ' . $another_game->high_score;
Memory Footprint
It’s obvious that caching values with descriptors saves you from executing expensive operations multiple times, but what about the amount of memory used? When you cache a variable locally per-instance, you’re storing that information once for each instance that requests it. Descriptors use far less memory in this case by only storing one copy. This idea can be extended into realms other than simple caching, and will always reap the rewards of leaner memory usage.
Cleaner Than the Alternatives
Implementing a dynamic property with the __get magic method requires checking the name of the requested property to determine whether or not it should be dynamically handled. Once that is determined, the __get method must then figure out what to actually do to calculate the dynamic value. Innumerable approaches to this problem exist in the wild, and that’s a problem in itself. There exists no standard way of determining what should be called, when, and in which context with traditional dynamic variables. Descriptors provide a standardized interface to these concepts, and don’t require hacky switch statements with string checking, or a gazillion setSomething and getSomething stop-gap methods. Descriptions are cleaner and easier.
True Object Oriented Design
Whether or not you’re an OOP lover (or even a hater) doesn’t matter. You have to agree that this confusing middle ground in which PHP has been living sucks. Descriptors promote the concept of PHP classes as objects themselves, which is what they really are. This is a good move for the language if it is even going to try to compete with the Pythons and Rubys of tomorrow. If functionality is class-wide, move it up to the class logic level where it belongs!
Conclusion
Descriptors are fun, they’re cool, and they’re just an all around good tool to have in your toolbox. They’re the ideal solution to some very complex, real world problems that have plagued developers for years. I long for the day when PHP releases support for dynamic class properties as well so we can get our hands on a drop-in replacement for Python’s descriptors. Until then, the descriptor interfaces and descriptable object class we’ve just designed will fit the bill just fine.