397 lines
9.5 KiB
PHP

<?php
/**
* This file is part of the Tracy (https://tracy.nette.org)
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
*/
declare(strict_types=1);
namespace Tracy\Dumper;
use Tracy;
use Tracy\Helpers;
/**
* Converts PHP values to internal representation.
* @internal
*/
final class Describer
{
public const HiddenValue = '*****';
// Number.MAX_SAFE_INTEGER
private const JsSafeInteger = 1 << 53 - 1;
/** @var int */
public $maxDepth = 7;
/** @var int */
public $maxLength = 150;
/** @var int */
public $maxItems = 100;
/** @var Value[] */
public $snapshot = [];
/** @var bool */
public $debugInfo = false;
/** @var array */
public $keysToHide = [];
/** @var callable|null fn(string $key, mixed $val): bool */
public $scrubber;
/** @var bool */
public $location = false;
/** @var callable[] */
public $resourceExposers;
/** @var array<string,callable> */
public $objectExposers;
/** @var (int|\stdClass)[] */
public $references = [];
public function describe($var): \stdClass
{
uksort($this->objectExposers, function ($a, $b): int {
return $b === '' || (class_exists($a, false) && is_subclass_of($a, $b)) ? -1 : 1;
});
try {
return (object) [
'value' => $this->describeVar($var),
'snapshot' => $this->snapshot,
'location' => $this->location ? self::findLocation() : null,
];
} finally {
$free = [[], []];
$this->snapshot = &$free[0];
$this->references = &$free[1];
}
}
/**
* @return mixed
*/
private function describeVar($var, int $depth = 0, ?int $refId = null)
{
if ($var === null || is_bool($var)) {
return $var;
}
$m = 'describe' . explode(' ', gettype($var))[0];
return $this->$m($var, $depth, $refId);
}
/**
* @return Value|int
*/
private function describeInteger(int $num)
{
return $num <= self::JsSafeInteger && $num >= -self::JsSafeInteger
? $num
: new Value(Value::TypeNumber, "$num");
}
/**
* @return Value|float
*/
private function describeDouble(float $num)
{
if (!is_finite($num)) {
return new Value(Value::TypeNumber, (string) $num);
}
$js = json_encode($num);
return strpos($js, '.')
? $num
: new Value(Value::TypeNumber, "$js.0"); // to distinct int and float in JS
}
/**
* @return Value|string
*/
private function describeString(string $s, int $depth = 0)
{
$encoded = Helpers::encodeString($s, $depth ? $this->maxLength : null);
if ($encoded === $s) {
return $encoded;
} elseif (Helpers::isUtf8($s)) {
return new Value(Value::TypeStringHtml, $encoded, Helpers::utf8Length($s));
} else {
return new Value(Value::TypeBinaryHtml, $encoded, strlen($s));
}
}
/**
* @return Value|array
*/
private function describeArray(array $arr, int $depth = 0, ?int $refId = null)
{
if ($refId) {
$res = new Value(Value::TypeRef, 'p' . $refId);
$value = &$this->snapshot[$res->value];
if ($value && $value->depth <= $depth) {
return $res;
}
$value = new Value(Value::TypeArray);
$value->id = $res->value;
$value->depth = $depth;
if ($this->maxDepth && $depth >= $this->maxDepth) {
$value->length = count($arr);
return $res;
} elseif ($depth && $this->maxItems && count($arr) > $this->maxItems) {
$value->length = count($arr);
$arr = array_slice($arr, 0, $this->maxItems, true);
}
$items = &$value->items;
} elseif ($arr && $this->maxDepth && $depth >= $this->maxDepth) {
return new Value(Value::TypeArray, null, count($arr));
} elseif ($depth && $this->maxItems && count($arr) > $this->maxItems) {
$res = new Value(Value::TypeArray, null, count($arr));
$res->depth = $depth;
$items = &$res->items;
$arr = array_slice($arr, 0, $this->maxItems, true);
}
$items = [];
foreach ($arr as $k => $v) {
$refId = $this->getReferenceId($arr, $k);
$items[] = [
$this->describeVar($k, $depth + 1),
$this->isSensitive((string) $k, $v)
? new Value(Value::TypeText, self::hideValue($v))
: $this->describeVar($v, $depth + 1, $refId),
] + ($refId ? [2 => $refId] : []);
}
return $res ?? $items;
}
private function describeObject(object $obj, int $depth = 0): Value
{
$id = spl_object_id($obj);
$value = &$this->snapshot[$id];
if ($value && $value->depth <= $depth) {
return new Value(Value::TypeRef, $id);
}
$value = new Value(Value::TypeObject, Helpers::getClass($obj));
$value->id = $id;
$value->depth = $depth;
$value->holder = $obj; // to be not released by garbage collector in collecting mode
if ($this->location) {
$rc = $obj instanceof \Closure
? new \ReflectionFunction($obj)
: new \ReflectionClass($obj);
if ($rc->getFileName() && ($editor = Helpers::editorUri($rc->getFileName(), $rc->getStartLine()))) {
$value->editor = (object) ['file' => $rc->getFileName(), 'line' => $rc->getStartLine(), 'url' => $editor];
}
}
if ($this->maxDepth && $depth < $this->maxDepth) {
$value->items = [];
$props = $this->exposeObject($obj, $value);
foreach ($props ?? [] as $k => $v) {
$this->addPropertyTo($value, (string) $k, $v, Value::PropertyVirtual, $this->getReferenceId($props, $k));
}
}
return new Value(Value::TypeRef, $id);
}
/**
* @param resource $resource
*/
private function describeResource($resource, int $depth = 0): Value
{
$id = 'r' . (int) $resource;
$value = &$this->snapshot[$id];
if (!$value) {
$type = is_resource($resource) ? get_resource_type($resource) : 'closed';
$value = new Value(Value::TypeResource, $type . ' resource');
$value->id = $id;
$value->depth = $depth;
$value->items = [];
if (isset($this->resourceExposers[$type])) {
foreach (($this->resourceExposers[$type])($resource) as $k => $v) {
$value->items[] = [htmlspecialchars($k), $this->describeVar($v, $depth + 1)];
}
}
}
return new Value(Value::TypeRef, $id);
}
/**
* @return Value|string
*/
public function describeKey(string $key)
{
if (preg_match('#^[\w!\#$%&*+./;<>?@^{|}~-]{1,50}$#D', $key) && !preg_match('#^(true|false|null)$#iD', $key)) {
return $key;
}
$value = $this->describeString($key);
return is_string($value) // ensure result is Value
? new Value(Value::TypeStringHtml, $key, Helpers::utf8Length($key))
: $value;
}
public function addPropertyTo(
Value $value,
string $k,
$v,
$type = Value::PropertyVirtual,
?int $refId = null,
?string $class = null
) {
if ($value->depth && $this->maxItems && count($value->items ?? []) >= $this->maxItems) {
$value->length = ($value->length ?? count($value->items)) + 1;
return;
}
$class = $class ?? $value->value;
$value->items[] = [
$this->describeKey($k),
$type !== Value::PropertyVirtual && $this->isSensitive($k, $v, $class)
? new Value(Value::TypeText, self::hideValue($v))
: $this->describeVar($v, $value->depth + 1, $refId),
$type === Value::PropertyPrivate ? $class : $type,
] + ($refId ? [3 => $refId] : []);
}
private function exposeObject(object $obj, Value $value): ?array
{
foreach ($this->objectExposers as $type => $dumper) {
if (!$type || $obj instanceof $type) {
return $dumper($obj, $value, $this);
}
}
if ($this->debugInfo && method_exists($obj, '__debugInfo')) {
return $obj->__debugInfo();
}
Exposer::exposeObject($obj, $value, $this);
return null;
}
private function isSensitive(string $key, $val, ?string $class = null): bool
{
return $val instanceof \SensitiveParameterValue
|| ($this->scrubber !== null && ($this->scrubber)($key, $val, $class))
|| isset($this->keysToHide[strtolower($key)])
|| isset($this->keysToHide[strtolower($class . '::$' . $key)]);
}
private static function hideValue($val): string
{
if ($val instanceof \SensitiveParameterValue) {
$val = $val->getValue();
}
return self::HiddenValue . ' (' . (is_object($val) ? Helpers::getClass($val) : gettype($val)) . ')';
}
public function getReferenceId($arr, $key): ?int
{
if (PHP_VERSION_ID >= 70400) {
if ((!$rr = \ReflectionReference::fromArrayElement($arr, $key))) {
return null;
}
$tmp = &$this->references[$rr->getId()];
if ($tmp === null) {
return $tmp = count($this->references);
}
return $tmp;
}
$uniq = new \stdClass;
$copy = $arr;
$orig = $copy[$key];
$copy[$key] = $uniq;
if ($arr[$key] !== $uniq) {
return null;
}
$res = array_search($uniq, $this->references, true);
$copy[$key] = $orig;
if ($res === false) {
$this->references[] = &$arr[$key];
return count($this->references);
}
return $res + 1;
}
/**
* Finds the location where dump was called. Returns [file, line, code]
*/
private static function findLocation(): ?array
{
foreach (debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS) as $item) {
if (isset($item['class']) && ($item['class'] === self::class || $item['class'] === Tracy\Dumper::class)) {
$location = $item;
continue;
} elseif (isset($item['function'])) {
try {
$reflection = isset($item['class'])
? new \ReflectionMethod($item['class'], $item['function'])
: new \ReflectionFunction($item['function']);
if (
$reflection->isInternal()
|| preg_match('#\s@tracySkipLocation\s#', (string) $reflection->getDocComment())
) {
$location = $item;
continue;
}
} catch (\ReflectionException $e) {
}
}
break;
}
if (isset($location['file'], $location['line']) && @is_file($location['file'])) { // @ - may trigger error
$lines = file($location['file']);
$line = $lines[$location['line'] - 1];
return [
$location['file'],
$location['line'],
trim(preg_match('#\w*dump(er::\w+)?\(.*\)#i', $line, $m) ? $m[0] : $line),
];
}
return null;
}
}