protected function evaluate(&$subject, $specialStr = false)
{
switch ($type = gettype($subject)) {
// https://github.com/digitalnature/php-ref/issues/13
case 'unknown type':
return $this->fmt->text('unknown');
// null value
// null value
case 'NULL':
return $this->fmt->text('null');
// integer/double/float
// integer/double/float
case 'integer':
case 'double':
return $this->fmt->text($type, $subject, $type);
// boolean
// boolean
case 'boolean':
$text = $subject ? 'true' : 'false';
return $this->fmt->text($text, $text, $type);
// arrays
// arrays
case 'array':
// empty array?
if (empty($subject)) {
$this->fmt->text('array');
return $this->fmt->emptyGroup();
}
if (isset($subject[static::MARKER_KEY])) {
unset($subject[static::MARKER_KEY]);
$this->fmt->text('array');
$this->fmt->emptyGroup('recursion');
return;
}
// first recursion level detection;
// this is optional (used to print consistent recursion info)
foreach ($subject as $key => &$value) {
if (!is_array($value)) {
continue;
}
// save current value in a temporary variable
$buffer = $value;
// assign new value
$value = $value !== 1 ? 1 : 2;
// if they're still equal, then we have a reference
if ($value === $subject) {
$value = $buffer;
$value[static::MARKER_KEY] = true;
$this->evaluate($value);
return;
}
// restoring original value
$value = $buffer;
}
$this->fmt->text('array');
$count = count($subject);
if (!$this->fmt->startGroup($count)) {
return;
}
$max = max(array_map('static::strLen', array_keys($subject)));
$subject[static::MARKER_KEY] = true;
foreach ($subject as $key => &$value) {
// ignore our temporary marker
if ($key === static::MARKER_KEY) {
continue;
}
if ($this->hasInstanceTimedOut()) {
break;
}
$keyInfo = gettype($key);
if ($keyInfo === 'string') {
$encoding = static::$env['mbStr'] ? mb_detect_encoding($key) : '';
$keyLen = $encoding && $encoding !== 'ASCII' ? static::strLen($key) . '; ' . $encoding : static::strLen($key);
$keyInfo = "{$keyInfo}({$keyLen})";
} else {
$keyLen = strlen($key);
}
$this->fmt->startRow();
$this->fmt->text('key', $key, "Key: {$keyInfo}");
$this->fmt->colDiv($max - $keyLen);
$this->fmt->sep('=>');
$this->fmt->colDiv();
$this->evaluate($value, $specialStr);
$this->fmt->endRow();
}
unset($subject[static::MARKER_KEY]);
$this->fmt->endGroup();
return;
// resource
// resource
case 'resource':
$meta = array();
$resType = get_resource_type($subject);
$this->fmt->text('resource', strval($subject));
if (!static::$config['showResourceInfo']) {
return $this->fmt->emptyGroup($resType);
}
// @see: http://php.net/manual/en/resource.php
// need to add more...
switch ($resType) {
// curl extension resource
case 'curl':
$meta = curl_getinfo($subject);
break;
case 'FTP Buffer':
$meta = array('time_out' => ftp_get_option($subject, FTP_TIMEOUT_SEC), 'auto_seek' => ftp_get_option($subject, FTP_AUTOSEEK));
break;
// gd image extension resource
// gd image extension resource
case 'gd':
$meta = array('size' => sprintf('%d x %d', imagesx($subject), imagesy($subject)), 'true_color' => imageistruecolor($subject));
break;
case 'ldap link':
$constants = get_defined_constants();
array_walk($constants, function ($value, $key) use(&$constants) {
if (strpos($key, 'LDAP_OPT_') !== 0) {
unset($constants[$key]);
}
});
// this seems to fail on my setup :(
unset($constants['LDAP_OPT_NETWORK_TIMEOUT']);
foreach (array_slice($constants, 3) as $key => $value) {
if (ldap_get_option($subject, (int) $value, $ret)) {
$meta[strtolower(substr($key, 9))] = $ret;
}
}
break;
// mysql connection (mysql extension is deprecated from php 5.4/5.5)
// mysql connection (mysql extension is deprecated from php 5.4/5.5)
case 'mysql link':
case 'mysql link persistent':
$dbs = array();
$query = @mysql_list_dbs($subject);
while ($row = @mysql_fetch_array($query)) {
$dbs[] = $row['Database'];
}
$meta = array('host' => ltrim(@mysql_get_host_info($subject), 'MySQL host info: '), 'server_version' => @mysql_get_server_info($subject), 'protocol_version' => @mysql_get_proto_info($subject), 'databases' => $dbs);
break;
// mysql result
// mysql result
case 'mysql result':
while ($row = @mysql_fetch_object($subject)) {
$meta[] = (array) $row;
if ($this->hasInstanceTimedOut()) {
break;
}
}
break;
// stream resource (fopen, fsockopen, popen, opendir etc)
// stream resource (fopen, fsockopen, popen, opendir etc)
case 'stream':
$meta = stream_get_meta_data($subject);
break;
}
if (!$meta) {
return $this->fmt->emptyGroup($resType);
}
if (!$this->fmt->startGroup($resType)) {
return;
}
$max = max(array_map('static::strLen', array_keys($meta)));
foreach ($meta as $key => $value) {
$this->fmt->startRow();
$this->fmt->text('resourceProp', ucwords(str_replace('_', ' ', $key)));
$this->fmt->colDiv($max - static::strLen($key));
$this->fmt->sep(':');
$this->fmt->colDiv();
$this->evaluate($value);
$this->fmt->endRow();
}
$this->fmt->endGroup();
return;
// string
// string
case 'string':
$length = static::strLen($subject);
$encoding = static::$env['mbStr'] ? mb_detect_encoding($subject) : false;
$info = $encoding && $encoding !== 'ASCII' ? $length . '; ' . $encoding : $length;
if ($specialStr) {
$this->fmt->sep('"');
$this->fmt->text(array('string', 'special'), $subject, "string({$info})");
$this->fmt->sep('"');
return;
}
$this->fmt->text('string', $subject, "string({$info})");
// advanced checks only if there are 3 characteres or more
if (static::$config['showStringMatches'] && $length > 2 && trim($subject) !== '') {
$isNumeric = is_numeric($subject);
// very simple check to determine if the string could match a file path
// @note: this part of the code is very expensive
$isFile = $length < 2048 && max(array_map('strlen', explode('/', str_replace('\\', '/', $subject)))) < 128 && !preg_match('/[^\\w\\.\\-\\/\\\\:]|\\..*\\.|\\.$|:(?!(?<=^[a-zA-Z]:)[\\/\\\\])/', $subject);
if ($isFile) {
try {
$file = new \SplFileInfo($subject);
$flags = array();
$perms = $file->getPerms();
if (($perms & 0xc000) === 0xc000) {
// socket
$flags[] = 's';
} elseif (($perms & 0xa000) === 0xa000) {
// symlink
$flags[] = 'l';
} elseif (($perms & 0x8000) === 0x8000) {
// regular
$flags[] = '-';
} elseif (($perms & 0x6000) === 0x6000) {
// block special
$flags[] = 'b';
} elseif (($perms & 0x4000) === 0x4000) {
// directory
$flags[] = 'd';
} elseif (($perms & 0x2000) === 0x2000) {
// character special
$flags[] = 'c';
} elseif (($perms & 0x1000) === 0x1000) {
// FIFO pipe
$flags[] = 'p';
} else {
// unknown
$flags[] = 'u';
}
// owner
$flags[] = $perms & 0x100 ? 'r' : '-';
$flags[] = $perms & 0x80 ? 'w' : '-';
$flags[] = $perms & 0x40 ? $perms & 0x800 ? 's' : 'x' : ($perms & 0x800 ? 'S' : '-');
// group
$flags[] = $perms & 0x20 ? 'r' : '-';
$flags[] = $perms & 0x10 ? 'w' : '-';
$flags[] = $perms & 0x8 ? $perms & 0x400 ? 's' : 'x' : ($perms & 0x400 ? 'S' : '-');
// world
$flags[] = $perms & 0x4 ? 'r' : '-';
$flags[] = $perms & 0x2 ? 'w' : '-';
$flags[] = $perms & 0x1 ? $perms & 0x200 ? 't' : 'x' : ($perms & 0x200 ? 'T' : '-');
$size = is_dir($subject) ? '' : sprintf(' %.2fK', $file->getSize() / 1024);
$this->fmt->startContain('file', true);
$this->fmt->text('file', implode('', $flags) . $size);
$this->fmt->endContain();
} catch (\Exception $e) {
$isFile = false;
}
}
// class/interface/function
if (!preg_match('/[^\\w+\\\\]/', $subject) && $length < 96) {
$isClass = class_exists($subject, false);
if ($isClass) {
$this->fmt->startContain('class', true);
$this->fromReflector(new \ReflectionClass($subject));
$this->fmt->endContain();
}
if (!$isClass && interface_exists($subject, false)) {
$this->fmt->startContain('interface', true);
$this->fromReflector(new \ReflectionClass($subject));
$this->fmt->endContain('interface');
}
if (function_exists($subject)) {
$this->fmt->startContain('function', true);
$this->fromReflector(new \ReflectionFunction($subject));
$this->fmt->endContain('function');
}
}
// skip serialization/json/date checks if the string appears to be numeric,
// or if it's shorter than 5 characters
if (!$isNumeric && $length > 4) {
// url
if (static::$config['showUrls'] && static::$env['curlActive'] && filter_var($subject, FILTER_VALIDATE_URL)) {
$ch = curl_init($subject);
curl_setopt($ch, CURLOPT_NOBODY, true);
curl_exec($ch);
$nfo = curl_getinfo($ch);
curl_close($ch);
if ($nfo['http_code']) {
$this->fmt->startContain('url', true);
$contentType = explode(';', $nfo['content_type']);
$this->fmt->text('url', sprintf('%s:%d %s %.2fms (%d)', !empty($nfo['primary_ip']) ? $nfo['primary_ip'] : null, !empty($nfo['primary_port']) ? $nfo['primary_port'] : null, $contentType[0], $nfo['total_time'], $nfo['http_code']));
$this->fmt->endContain();
}
}
// date
if ($length < 128 && static::$env['supportsDate'] && !preg_match('/[^A-Za-z0-9.:+\\s\\-\\/]/', $subject)) {
try {
$date = new \DateTime($subject);
$errors = \DateTime::getLastErrors();
if ($errors['warning_count'] < 1 && $errors['error_count'] < 1) {
$now = new \Datetime('now');
$nowUtc = new \Datetime('now', new \DateTimeZone('UTC'));
$diff = $now->diff($date);
$map = array('y' => 'yr', 'm' => 'mo', 'd' => 'da', 'h' => 'hr', 'i' => 'min', 's' => 'sec');
$timeAgo = 'now';
foreach ($map as $k => $label) {
if ($diff->{$k} > 0) {
$timeAgo = $diff->format("%R%{$k}{$label}");
break;
}
}
$tz = $date->getTimezone();
$offs = round($tz->getOffset($nowUtc) / 3600);
if ($offs > 0) {
$offs = "+{$offs}";
}
$timeAgo .= (int) $offs !== 0 ? ' ' . sprintf('%s (UTC%s)', $tz->getName(), $offs) : ' UTC';
$this->fmt->startContain('date', true);
$this->fmt->text('date', $timeAgo);
$this->fmt->endContain();
}
} catch (\Exception $e) {
// not a date
}
}
// attempt to detect if this is a serialized string
static $unserializing = 0;
$isSerialized = $unserializing < 3 && ($subject[$length - 1] === ';' || $subject[$length - 1] === '}') && in_array($subject[0], array('s', 'a', 'O'), true) && ($subject[0] === 's' && $subject[$length - 2] !== '"' || preg_match("/^{$subject[0]}:[0-9]+:/s", $subject)) && ($unserialized = @unserialize($subject)) !== false;
if ($isSerialized) {
$unserializing++;
$this->fmt->startContain('serialized', true);
$this->evaluate($unserialized);
$this->fmt->endContain();
$unserializing--;
}
// try to find out if it's a json-encoded string;
// only do this for json-encoded arrays or objects, because other types have too generic formats
static $decodingJson = 0;
$isJson = !$isSerialized && $decodingJson < 3 && in_array($subject[0], array('{', '['), true);
if ($isJson) {
$decodingJson++;
$json = json_decode($subject);
if ($isJson = json_last_error() === JSON_ERROR_NONE) {
$this->fmt->startContain('json', true);
$this->evaluate($json);
$this->fmt->endContain();
}
$decodingJson--;
}
// attempt to match a regex
if ($length < 768) {
try {
$components = $this->splitRegex($subject);
if ($components) {
$regex = '';
$this->fmt->startContain('regex', true);
foreach ($components as $component) {
$this->fmt->text('regex-' . key($component), reset($component));
}
$this->fmt->endContain();
}
} catch (\Exception $e) {
// not a regex
}
}
}
}
return;
}
// if we reached this point, $subject must be an object
// track objects to detect recursion
static $hashes = array();
// hash ID of this object
$hash = spl_object_hash($subject);
$recursion = isset($hashes[$hash]);
// sometimes incomplete objects may be created from string unserialization,
// if the class to which the object belongs wasn't included until the unserialization stage...
if ($subject instanceof \__PHP_Incomplete_Class) {
$this->fmt->text('object');
$this->fmt->emptyGroup('incomplete');
return;
}
// check cache at this point
if (!$recursion && $this->fmt->didCache($hash)) {
static::$debug['cacheHits']++;
return;
}
$reflector = new \ReflectionObject($subject);
$this->fmt->startContain('class');
$this->fromReflector($reflector);
$this->fmt->text('object', ' object');
$this->fmt->endContain();
// already been here?
if ($recursion) {
return $this->fmt->emptyGroup('recursion');
}
$hashes[$hash] = 1;
$flags = \ReflectionProperty::IS_PUBLIC | \ReflectionProperty::IS_PROTECTED;
if (static::$config['showPrivateMembers']) {
$flags |= \ReflectionProperty::IS_PRIVATE;
}
$props = $reflector->getProperties($flags);
$methods = array();
if (static::$config['showMethods']) {
$flags = \ReflectionMethod::IS_PUBLIC | \ReflectionMethod::IS_PROTECTED;
if (static::$config['showPrivateMembers']) {
$flags |= \ReflectionMethod::IS_PRIVATE;
}
$methods = $reflector->getMethods($flags);
}
$constants = $reflector->getConstants();
$interfaces = $reflector->getInterfaces();
$traits = static::$env['is54'] ? $reflector->getTraits() : array();
$parents = static::getParentClasses($reflector);
// work-around for https://bugs.php.net/bug.php?id=49154
// @see http://stackoverflow.com/questions/15672287/strange-behavior-of-reflectiongetproperties-with-numeric-keys
if (!static::$env['is54']) {
$props = array_values(array_filter($props, function ($prop) use($subject) {
return !$prop->isPublic() || property_exists($subject, $prop->name);
}));
}
// no data to display?
if (!$props && !$methods && !$constants && !$interfaces && !$traits) {
unset($hashes[$hash]);
return $this->fmt->emptyGroup();
}
if (!$this->fmt->startGroup()) {
return;
}
// show contents for iterators
if (static::$config['showIteratorContents'] && $reflector->isIterateable()) {
$itContents = iterator_to_array($subject);
$this->fmt->sectionTitle(sprintf('Contents (%d)', count($itContents)));
foreach ($itContents as $key => $value) {
$keyInfo = gettype($key);
if ($keyInfo === 'string') {
$encoding = static::$env['mbStr'] ? mb_detect_encoding($key) : '';
$length = $encoding && $encoding !== 'ASCII' ? static::strLen($key) . '; ' . $encoding : static::strLen($key);
$keyInfo = sprintf('%s(%s)', $keyInfo, $length);
}
$this->fmt->startRow();
$this->fmt->text(array('key', 'iterator'), $key, sprintf('Iterator key: %s', $keyInfo));
$this->fmt->colDiv();
$this->fmt->sep('=>');
$this->fmt->colDiv();
$this->evaluate($value);
//$this->evaluate($value instanceof \Traversable ? ((count($value) > 0) ? $value : (string)$value) : $value);
$this->fmt->endRow();
}
}
// display the interfaces this objects' class implements
if ($interfaces) {
$items = array();
$this->fmt->sectionTitle('Implements');
$this->fmt->startRow();
$this->fmt->startContain('interfaces');
$i = 0;
$count = count($interfaces);
foreach ($interfaces as $name => $interface) {
$this->fromReflector($interface);
if (++$i < $count) {
$this->fmt->sep(', ');
}
}
$this->fmt->endContain();
$this->fmt->endRow();
}
// traits this objects' class uses
if ($traits) {
$items = array();
$this->fmt->sectionTitle('Uses');
$this->fmt->startRow();
$this->fmt->startContain('traits');
$i = 0;
$count = count($traits);
foreach ($traits as $name => $trait) {
$this->fromReflector($trait);
if (++$i < $count) {
$this->fmt->sep(', ');
}
}
$this->fmt->endContain();
$this->fmt->endRow();
}
// class constants
if ($constants) {
$this->fmt->sectionTitle('Constants');
$max = max(array_map('static::strLen', array_keys($constants)));
foreach ($constants as $name => $value) {
$meta = null;
$type = array('const');
foreach ($parents as $parent) {
if ($parent->hasConstant($name)) {
if ($parent !== $reflector) {
$type[] = 'inherited';
$meta = array('sub' => array(array('Prototype defined by', $parent->name)));
}
break;
}
}
$this->fmt->startRow();
$this->fmt->sep('::');
$this->fmt->colDiv();
$this->fmt->startContain($type);
$this->fmt->text('name', $name, $meta, $this->linkify($parent, $name));
$this->fmt->endContain();
$this->fmt->colDiv($max - static::strLen($name));
$this->fmt->sep('=');
$this->fmt->colDiv();
$this->evaluate($value);
$this->fmt->endRow();
}
}
// object/class properties
if ($props) {
$this->fmt->sectionTitle('Properties');
$max = 0;
foreach ($props as $idx => $prop) {
if (($propNameLen = static::strLen($prop->name)) > $max) {
$max = $propNameLen;
}
}
foreach ($props as $idx => $prop) {
if ($this->hasInstanceTimedOut()) {
break;
}
$bubbles = array();
$sourceClass = $prop->getDeclaringClass();
$inherited = $reflector->getShortName() !== $sourceClass->getShortName();
$meta = $sourceClass->isInternal() ? null : static::parseComment($prop->getDocComment());
if ($meta) {
if ($inherited) {
$meta['sub'] = array(array('Declared in', $sourceClass->getShortName()));
}
if (isset($meta['tags']['var'][0])) {
$meta['left'] = $meta['tags']['var'][0][0];
}
unset($meta['tags']);
}
if ($prop->isProtected() || $prop->isPrivate()) {
$prop->setAccessible(true);
}
$value = $prop->getValue($subject);
$this->fmt->startRow();
$this->fmt->sep($prop->isStatic() ? '::' : '->');
$this->fmt->colDiv();
$bubbles = array();
if ($prop->isProtected()) {
$bubbles[] = array('P', 'Protected');
}
if ($prop->isPrivate()) {
$bubbles[] = array('!', 'Private');
}
$this->fmt->bubbles($bubbles);
$type = array('prop');
if ($inherited) {
$type[] = 'inherited';
}
if ($prop->isPrivate()) {
$type[] = 'private';
}
$this->fmt->colDiv(2 - count($bubbles));
$this->fmt->startContain($type);
$this->fmt->text('name', $prop->name, $meta, $this->linkify($prop));
$this->fmt->endContain();
$this->fmt->colDiv($max - static::strLen($prop->name));
$this->fmt->sep('=');
$this->fmt->colDiv();
$this->evaluate($value);
$this->fmt->endRow();
}
}
// class methods
if ($methods && !$this->hasInstanceTimedOut()) {
$this->fmt->sectionTitle('Methods');
foreach ($methods as $idx => $method) {
$this->fmt->startRow();
$this->fmt->sep($method->isStatic() ? '::' : '->');
$this->fmt->colDiv();
$bubbles = array();
if ($method->isAbstract()) {
$bubbles[] = array('A', 'Abstract');
}
if ($method->isFinal()) {
$bubbles[] = array('F', 'Final');
}
if ($method->isProtected()) {
$bubbles[] = array('P', 'Protected');
}
if ($method->isPrivate()) {
$bubbles[] = array('!', 'Private');
}
$this->fmt->bubbles($bubbles);
$this->fmt->colDiv(4 - count($bubbles));
// is this method inherited?
$inherited = $reflector->getShortName() !== $method->getDeclaringClass()->getShortName();
$type = array('method');
if ($inherited) {
$type[] = 'inherited';
}
if ($method->isPrivate()) {
$type[] = 'private';
}
$this->fmt->startContain($type);
$name = $method->name;
if ($method->returnsReference()) {
$name = "&{$name}";
}
$this->fromReflector($method, $name, $reflector);
$paramCom = $method->isInternal() ? array() : static::parseComment($method->getDocComment(), 'tags');
$paramCom = empty($paramCom['param']) ? array() : $paramCom['param'];
$paramCount = $method->getNumberOfParameters();
$this->fmt->sep('(');
// process arguments
foreach ($method->getParameters() as $idx => $parameter) {
$meta = null;
$paramName = "\${$parameter->name}";
$optional = $parameter->isOptional();
$variadic = static::$env['is56'] && $parameter->isVariadic();
if ($parameter->isPassedByReference()) {
$paramName = "&{$paramName}";
}
if ($variadic) {
$paramName = "...{$paramName}";
}
$type = array('param');
if ($optional) {
$type[] = 'optional';
}
$this->fmt->startContain($type);
// attempt to build meta
foreach ($paramCom as $tag) {
list($pcTypes, $pcName, $pcDescription) = $tag;
if ($pcName !== $paramName) {
continue;
}
$meta = array('title' => $pcDescription);
if ($pcTypes) {
$meta['left'] = $pcTypes;
}
break;
}
try {
$paramClass = $parameter->getClass();
} catch (\Exception $e) {
// @see https://bugs.php.net/bug.php?id=32177&edit=1
}
if (!empty($paramClass)) {
$this->fmt->startContain('hint');
$this->fromReflector($paramClass, $paramClass->name);
$this->fmt->endContain();
$this->fmt->sep(' ');
} elseif ($parameter->isArray()) {
$this->fmt->text('hint', 'array');
$this->fmt->sep(' ');
} else {
$hasType = static::$env['is7'] && $parameter->hasType();
if ($hasType) {
$type = $parameter->getType();
$this->fmt->text('hint', (string) $type);
$this->fmt->sep(' ');
}
}
$this->fmt->text('name', $paramName, $meta);
if ($optional) {
$paramValue = $parameter->isDefaultValueAvailable() ? $parameter->getDefaultValue() : null;
$this->fmt->sep(' = ');
if (static::$env['is546'] && !$parameter->getDeclaringFunction()->isInternal() && $parameter->isDefaultValueConstant()) {
$this->fmt->text('constant', $parameter->getDefaultValueConstantName(), 'Constant');
} else {
$this->evaluate($paramValue, true);
}
}
$this->fmt->endContain();
if ($idx < $paramCount - 1) {
$this->fmt->sep(', ');
}
}
$this->fmt->sep(')');
$this->fmt->endContain();
$hasReturnType = static::$env['is7'] && $method->hasReturnType();
if ($hasReturnType) {
$type = $method->getReturnType();
$this->fmt->startContain('ret');
$this->fmt->sep(':');
$this->fmt->text('hint', (string) $type);
$this->fmt->endContain();
}
$this->fmt->endRow();
}
}
unset($hashes[$hash]);
$this->fmt->endGroup();
$this->fmt->cacheLock($hash);
}