5 jul 2012

A dark corner of PHP: class casting » Jasny · web development

Sometimes you want things that simply look impossible in PHP. A few years ago I was using the DB library from PEAR. I wanted to add some functionality to the DB_Result class, however since the result object is created in the DB class and the class used for that isn’t configurable, I was kind of stuck there.
I solved this problem by writing a class extending the DB_Result class. The class had which used reflection to pull all the info out of the DB_Result class and created a new DB_Result_Ext object. In the end I decided that I didn’t like DB class at all (nor MDB2 for that mather), so I threw away this dirty code, however I always felt that there had to be a better way in solving these kind of issues.
The new solution for this problem I’m about to show, is really a dark corner of PHP and perhaps even dirtier than the other dirty solution. However the method can get you out of a jam when this situation arises.

PHP has a function serialize, which can create a hash from any type of variable, scalars, array, but objects as well. Using the unserialize function, PHP can recreate the variable from the serialized hashed. If we look at how an object is serialized, we see only the properties and the class name are stored. Serializing an object of the ‘mother’ class, with a protected variables ‘$myvar’, would give the following hash:
 
O:6:"mother":1:{s:8:"�*�myvar";i:5;}
 
We see an ‘O’ for object, the string length of the class name, the quoted class name and the properties. What would happen if we would change the class name and unserialize?

Let’s have a look at the following example:

/**
* Parent class
*/
class mother
{
protected $myvar;
function __construct($value)
{
$this->myvar = $value;
}
}
/**
* Child class
*/
class grandmother extends mother
{
protected $bval;
function __construct($value)
{
$this->bval = $value;
$this->myvar = $value / 10;
}
function __wakeup()
{
if (!isset($this->myvar)) throw new Exception("Illegal unserialize: The original object did not contain a myvar property.");
$this->bval = $this->myvar * 10;
}
function getVal() {
return $this->bval;
}
}
/**
* Cast an object to another class, keeping the properties, but changing the methods
*
* @param string $class Class name
* @param object $object
* @return object
*/
function casttoclass($class, $object)
{
return unserialize(preg_replace('/^O:\d+:"[^"]++"/', 'O:' . strlen($class) . ':"' . $class . '"', serialize($object)));
}
$a = new mother(5);
$z = casttoclass('grandmother', $a);
echo $z->getVal();


We have a class ‘mother’ and a class ‘grandmother’ extending ‘mother’. We create a ‘mother’ object and now we want to cast it to a ‘grandmother’. In the casttoclass() function, we serialize the object, use a preg_replace to change the class name and than unserialize the hash. This creates a ‘grandmother’ object, with a property ‘$myvar’ set to 5, however the ‘$bval’ property is not set. To solve this we can use the magic method __wakeup(). After the conversion, we can use $z normally as any other ‘grandmother’.