lib/Boris/ColoredInspector.php
<?php
/**
* @author Rob Morris <rob@irongaze.com>
* @author Chris Corbyn <chris@w3style.co.uk>
*
* Copyright © 2013-2014 Rob Morris.
*/
namespace Boris;
/**
* Identifies data types in data structures and syntax highlights them.
*/
class ColoredInspector implements Inspector
{
static $TERM_COLORS = array('black' => "\033[0;30m", 'white' => "\033[1;37m", 'none' => "\033[1;30m", 'dark_grey' => "\033[1;30m", 'light_grey' => "\033[0;37m", 'dark_red' => "\033[0;31m", 'light_red' => "\033[1;31m", 'dark_green' => "\033[0;32m", 'light_green' => "\033[1;32m", 'dark_yellow' => "\033[0;33m", 'light_yellow' => "\033[1;33m", 'dark_blue' => "\033[0;34m", 'light_blue' => "\033[1;34m", 'dark_purple' => "\033[0;35m", 'light_purple' => "\033[1;35m", 'dark_cyan' => "\033[0;36m", 'light_cyan' => "\033[1;36m");
private $_fallback;
private $_colorMap = array();
/**
* Initialize a new ColoredInspector, using $colorMap.
*
* The colors should be an associative array with the keys:
*
* - 'integer'
* - 'float'
* - 'keyword'
* - 'string'
* - 'boolean'
* - 'default'
*
* And the values, one of the following colors:
*
* - 'none'
* - 'black'
* - 'white'
* - 'dark_grey'
* - 'light_grey'
* - 'dark_red'
* - 'light_red'
* - 'dark_green'
* - 'light_green'
* - 'dark_yellow'
* - 'light_yellow'
* - 'dark_blue'
* - 'light_blue'
* - 'dark_purple'
* - 'light_purple'
* - 'dark_cyan'
* - 'light_cyan'
*
* An empty $colorMap array effectively means 'none' for all types.
*
* @param array $colorMap
*/
public function __construct($colorMap = null)
{
$this->_fallback = new DumpInspector();
if (isset($colorMap)) {
$this->_colorMap = $colorMap;
} else {
$this->_colorMap = $this->_defaultColorMap();
}
}
public function inspect($variable)
{
return preg_replace('/^/m', $this->_colorize('comment', '// '), $this->_dump($variable));
}
/**
* Returns an associative array of an object's properties.
*
* This method is public so that subclasses may override it.
*
* @param object $value
* @return array
* */
public function objectVars($value)
{
return get_object_vars($value);
}
// -- Private Methods
public function _dump($value)
{
$tests = array(
'is_null' => '_dumpNull',
'is_string' => '_dumpString',
'is_bool' => '_dumpBoolean',
'is_integer' => '_dumpInteger',
'is_float' => '_dumpFloat',
'is_array' => '_dumpArray',
'is_object' => '_dumpObject'
);
foreach ($tests as $predicate => $outputMethod) {
if (call_user_func($predicate, $value))
return call_user_func(array(
$this,
$outputMethod
), $value);
}
return $this->_fallback->inspect($value);
}
private function _dumpNull($value)
{
return $this->_colorize('keyword', 'NULL');
}
private function _dumpString($value)
{
return $this->_colorize('string', var_export($value, true));
}
private function _dumpBoolean($value)
{
return $this->_colorize('bool', var_export($value, true));
}
private function _dumpInteger($value)
{
return $this->_colorize('integer', var_export($value, true));
}
private function _dumpFloat($value)
{
return $this->_colorize('float', var_export($value, true));
}
private function _dumpArray($value)
{
return $this->_dumpStructure('array', $value);
}
private function _dumpObject($value)
{
return $this->_dumpStructure(sprintf('object(%s)', get_class($value)), $this->objectVars($value));
}
private function _dumpStructure($type, $value)
{
return $this->_astToString($this->_buildAst($type, $value));
}
public function _buildAst($type, $value, $seen = array())
{
// FIXME: Improve this AST so it doesn't require access to dump() or colorize()
if ($this->_isSeen($value, $seen)) {
return $this->_colorize('default', '*** RECURSION ***');
} else {
$nextSeen = array_merge($seen, array(
$value
));
}
if (is_object($value)) {
$vars = $this->objectVars($value);
} else {
$vars = $value;
}
$self = $this;
return array(
'name' => $this->_colorize('keyword', $type),
'children' => empty($vars) ? array() : array_combine(array_map(array(
$this,
'_dump'
), array_keys($vars)), array_map(function($v) use ($self, $nextSeen)
{
if (is_object($v)) {
return $self->_buildAst(sprintf('object(%s)', get_class($v)), $v, $nextSeen);
} elseif (is_array($v)) {
return $self->_buildAst('array', $v, $nextSeen);
} else {
return $self->_dump($v);
}
}, array_values($vars)))
);
}
public function _astToString($node, $indent = 0)
{
$children = $node['children'];
$self = $this;
return implode("\n", array(
sprintf('%s(', $node['name']),
implode(",\n", array_map(function($k) use ($self, $children, $indent)
{
if (is_array($children[$k])) {
return sprintf('%s%s => %s', str_repeat(' ', ($indent + 1) * 2), $k, $self->_astToString($children[$k], $indent + 1));
} else {
return sprintf('%s%s => %s', str_repeat(' ', ($indent + 1) * 2), $k, $children[$k]);
}
}, array_keys($children))),
sprintf('%s)', str_repeat(' ', $indent * 2))
));
}
private function _defaultColorMap()
{
return array(
'integer' => 'light_green',
'float' => 'light_yellow',
'string' => 'light_red',
'bool' => 'light_purple',
'keyword' => 'light_cyan',
'comment' => 'dark_grey',
'default' => 'none'
);
}
private function _colorize($type, $value)
{
if (!empty($this->_colorMap[$type])) {
$colorName = $this->_colorMap[$type];
} else {
$colorName = $this->_colorMap['default'];
}
return sprintf("%s%s\033[0m", static::$TERM_COLORS[$colorName], $value);
}
private function _isSeen($value, $seen)
{
foreach ($seen as $v) {
if ($v === $value)
return true;
}
return false;
}
}