How to Expose PHP’s Private Parts
I’ve been tinkering with dumping PHP objects, and have found myself constantly running into a brick wall. The output from print_r
and friends is fine in some contexts, but for larger structures, it would be nice to tidy the output up a bit and wrap it in some HTML.
However, these functions have a certain privilege in that they can access private/protected variables. This is not something that can be circumvented in plain PHP (save through some exotic extensions, which I’d rather not rely on). It seems that 5.3.0 may introduce a functionality in the Reflection API, but nobody knows when 5.3.0 will be out.
So I realised that it is possible to serialize any object to a string, which will include private and protected variables alike. All I had to do was then to parse the serialized string, and I’d gain that arcane insight. One would perhaps assume that such a parser already exists. Maybe it does, but I couldn’t find it (I did find a Java implementation though). The format is fairly simple though, so I spend half a Sunday afternoon on it. One very nice bonus, compared to print_r
is that the format handles recursion, which is very handy
for dumping the output in a presentable form.
So, without further ado, here’s the code:
< ?php
/**
* Exports variable information, including private/protected variables
and with recursion-protection.
* Since this is built upon PHP serialization functionality,
unserializable objects may cause trouble.
*/
class XrayVision {
protected $id;
function export($object) {
$this->id = 1;
list($value, $input) = $this->parse(serialize($object));
return $value;
}
protected function parse($input) {
if (substr($input, 0, 2) === 'N;') {
return array(array('type' => 'null', 'id' => $this->id++,
'value' => null), substr($input, 2));
}
$pos = strpos($input, ':');
$type = substr($input, 0, $pos);
$input = substr($input, $pos + 1);
switch ($type) {
case 's':
return $this->s($input);
case 'i':
return $this->i($input);
case 'd':
return $this->d($input);
case 'b':
return $this->b($input);
case 'O':
return $this->o($input);
case 'a':
return $this->a($input);
case 'r':
return $this->r($input);
}
throw new Exception("Unhandled type '$type'");
}
protected function s($input) {
$pos = strpos($input, ':');
$length = substr($input, 0, $pos);
$input = substr($input, $pos + 1);
$value = substr($input, 1, $length);
return array(array('type' => 'string', 'id' => $this->id++,
'value' => $value), substr($input, $length + 3));
}
protected function i($input) {
$pos = strpos($input, ';');
$value = (integer) substr($input, 0, $pos);
return array(array('type' => 'integer', 'id' => $this->id++,
'value' => $value), substr($input, $pos + 1));
}
protected function d($input) {
$pos = strpos($input, ';');
$value = (float) substr($input, 0, $pos);
return array(array('type' => 'float', 'id' => $this->id++, 'value'
=> $value), substr($input, $pos + 1));
}
protected function b($input) {
$pos = strpos($input, ';');
$value = substr($input, 0, $pos) === '1';
return array(array('type' => 'boolean', 'id' => $this->id++,
'value' => $value), substr($input, $pos + 1));
}
protected function r($input) {
$pos = strpos($input, ';');
$value = (integer) substr($input, 0, $pos);
return array(array('type' => 'recursion', 'id' => $this->id++,
'value' => $value), substr($input, $pos + 1));
}
protected function o($input) {
$id = $this->id++;
$pos = strpos($input, ':');
$name_length = substr($input, 0, $pos);
$input = substr($input, $pos + 1);
$name = substr($input, 1, $name_length);
$input = substr($input, $name_length + 3);
$pos = strpos($input, ':');
$length = (int) substr($input, 0, $pos);
$input = substr($input, $pos + 2);
$values = array();
for ($ii=0; $ii < $length; $ii++) {
list($key, $input) = $this->parse($input);
$this->id--;
list($value, $input) = $this->parse($input);
if (substr($key['value'], 0, 3) === "00*00") {
$values['protected:' . substr($key['value'], 3)] = $value;
} elseif ($pos = strrpos($key['value'], "00")) {
$values['private:' . substr($key['value'], $pos + 1)] = $value;
} else {
$values[str_replace("00", ':', $key['value'])] = $value;
}
}
return array(
array('type' => 'object', 'id' => $id, 'class' => $name, 'value'
=> $values),
substr($input, 1));
}
protected function a($input) {
$id = $this->id++;
$pos = strpos($input, ':');
$length = (int) substr($input, 0, $pos);
$input = substr($input, $pos + 2);
$values = array();
for ($ii=0; $ii < $length; $ii++) {
list($key, $input) = $this->parse($input);
$this->id--;
list($value, $input) = $this->parse($input);
$values[$key['value']] = $value;
}
return array(
array('type' => 'array', 'id' => $id, 'value' => $values),
substr($input, 1));
}
}
?>
There are at least two known issues with this technique. The first is that resources are serialized into integers. For a dump, this doesn’t really matter, since a resource is meaningless outside the running process. The other problem is with objects that implements __sleep
. Since this function may have side-effects, you can potentially mess up your program for objects that use this feature. In my experience, it’s a seldom used functionality anyway, so it doesn’t really bother me that much.