1: <?php
2:
3: 4: 5: 6:
7:
8: namespace Tracy;
9:
10:
11: 12: 13:
14: class Dumper
15: {
16: const
17: DEPTH = 'depth',
18: TRUNCATE = 'truncate',
19: COLLAPSE = 'collapse',
20: COLLAPSE_COUNT = 'collapsecount',
21: LOCATION = 'location',
22: OBJECT_EXPORTERS = 'exporters',
23: LIVE = 'live',
24: DEBUGINFO = 'debuginfo',
25: KEYS_TO_HIDE = 'keystohide';
26:
27: const
28: LOCATION_SOURCE = 0b0001,
29: LOCATION_LINK = 0b0010,
30: LOCATION_CLASS = 0b0100;
31:
32: const
33: HIDDEN_VALUE = '*****';
34:
35:
36: public static $terminalColors = [
37: 'bool' => '1;33',
38: 'null' => '1;33',
39: 'number' => '1;32',
40: 'string' => '1;36',
41: 'array' => '1;31',
42: 'key' => '1;37',
43: 'object' => '1;31',
44: 'visibility' => '1;30',
45: 'resource' => '1;37',
46: 'indent' => '1;30',
47: ];
48:
49:
50: public static $resources = [
51: 'stream' => 'stream_get_meta_data',
52: 'stream-context' => 'stream_context_get_options',
53: 'curl' => 'curl_getinfo',
54: ];
55:
56:
57: public static $objectExporters = [
58: 'Closure' => 'Tracy\Dumper::exportClosure',
59: 'SplFileInfo' => 'Tracy\Dumper::exportSplFileInfo',
60: 'SplObjectStorage' => 'Tracy\Dumper::exportSplObjectStorage',
61: '__PHP_Incomplete_Class' => 'Tracy\Dumper::exportPhpIncompleteClass',
62: ];
63:
64:
65: public static $livePrefix;
66:
67:
68: private static $liveStorage = [];
69:
70:
71: 72: 73: 74:
75: public static function dump($var, array $options = null)
76: {
77: if (PHP_SAPI !== 'cli' && !preg_match('#^Content-Type: (?!text/html)#im', implode("\n", headers_list()))) {
78: echo self::toHtml($var, $options);
79: } elseif (self::detectColors()) {
80: echo self::toTerminal($var, $options);
81: } else {
82: echo self::toText($var, $options);
83: }
84: return $var;
85: }
86:
87:
88: 89: 90: 91:
92: public static function toHtml($var, array $options = null)
93: {
94: $options = (array) $options + [
95: self::DEPTH => 4,
96: self::TRUNCATE => 150,
97: self::COLLAPSE => 14,
98: self::COLLAPSE_COUNT => 7,
99: self::OBJECT_EXPORTERS => null,
100: self::DEBUGINFO => false,
101: self::KEYS_TO_HIDE => [],
102: ];
103: $loc = &$options[self::LOCATION];
104: $loc = $loc === true ? ~0 : (int) $loc;
105:
106: $options[self::KEYS_TO_HIDE] = array_flip(array_map('strtolower', $options[self::KEYS_TO_HIDE]));
107: $options[self::OBJECT_EXPORTERS] = (array) $options[self::OBJECT_EXPORTERS] + self::$objectExporters;
108: uksort($options[self::OBJECT_EXPORTERS], function ($a, $b) {
109: return $b === '' || (class_exists($a, false) && is_subclass_of($a, $b)) ? -1 : 1;
110: });
111:
112: $live = !empty($options[self::LIVE]) && $var && (is_array($var) || is_object($var) || is_resource($var));
113: list($file, $line, $code) = $loc ? self::findLocation() : null;
114: $locAttrs = $file && $loc & self::LOCATION_SOURCE ? Helpers::formatHtml(
115: ' title="%in file % on line %" data-tracy-href="%"', "$code\n", $file, $line, Helpers::editorUri($file, $line)
116: ) : null;
117:
118: return '<pre class="tracy-dump' . ($live && $options[self::COLLAPSE] === true ? ' tracy-collapsed' : '') . '"'
119: . $locAttrs
120: . ($live ? " data-tracy-dump='" . json_encode(self::toJson($var, $options), JSON_HEX_APOS | JSON_HEX_AMP) . "'>" : '>')
121: . ($live ? '' : self::dumpVar($var, $options))
122: . ($file && $loc & self::LOCATION_LINK ? '<small>in ' . Helpers::editorLink($file, $line) . '</small>' : '')
123: . "</pre>\n";
124: }
125:
126:
127: 128: 129: 130:
131: public static function toText($var, array $options = null)
132: {
133: return htmlspecialchars_decode(strip_tags(self::toHtml($var, $options)), ENT_QUOTES);
134: }
135:
136:
137: 138: 139: 140:
141: public static function toTerminal($var, array $options = null)
142: {
143: return htmlspecialchars_decode(strip_tags(preg_replace_callback('#<span class="tracy-dump-(\w+)">|</span>#', function ($m) {
144: return "\033[" . (isset($m[1], self::$terminalColors[$m[1]]) ? self::$terminalColors[$m[1]] : '0') . 'm';
145: }, self::toHtml($var, $options))), ENT_QUOTES);
146: }
147:
148:
149: 150: 151: 152: 153: 154: 155:
156: private static function dumpVar(&$var, array $options, $level = 0)
157: {
158: if (method_exists(__CLASS__, $m = 'dump' . gettype($var))) {
159: return self::$m($var, $options, $level);
160: } else {
161: return "<span>unknown type</span>\n";
162: }
163: }
164:
165:
166: private static function dumpNull()
167: {
168: return "<span class=\"tracy-dump-null\">null</span>\n";
169: }
170:
171:
172: private static function dumpBoolean(&$var)
173: {
174: return '<span class="tracy-dump-bool">' . ($var ? 'true' : 'false') . "</span>\n";
175: }
176:
177:
178: private static function dumpInteger(&$var)
179: {
180: return "<span class=\"tracy-dump-number\">$var</span>\n";
181: }
182:
183:
184: private static function dumpDouble(&$var)
185: {
186: $var = is_finite($var)
187: ? ($tmp = json_encode($var)) . (strpos($tmp, '.') === false ? '.0' : '')
188: : str_replace('.0', '', var_export($var, true));
189: return "<span class=\"tracy-dump-number\">$var</span>\n";
190: }
191:
192:
193: private static function dumpString(&$var, $options)
194: {
195: return '<span class="tracy-dump-string">"'
196: . Helpers::escapeHtml(self::encodeString($var, $options[self::TRUNCATE]))
197: . '"</span>' . (strlen($var) > 1 ? ' (' . strlen($var) . ')' : '') . "\n";
198: }
199:
200:
201: private static function dumpArray(&$var, $options, $level)
202: {
203: static $marker;
204: if ($marker === null) {
205: $marker = uniqid("\x00", true);
206: }
207:
208: $out = '<span class="tracy-dump-array">array</span> (';
209:
210: if (empty($var)) {
211: return $out . ")\n";
212:
213: } elseif (isset($var[$marker])) {
214: return $out . (count($var) - 1) . ") [ <i>RECURSION</i> ]\n";
215:
216: } elseif (!$options[self::DEPTH] || $level < $options[self::DEPTH]) {
217: $collapsed = $level ? count($var) >= $options[self::COLLAPSE_COUNT]
218: : (is_int($options[self::COLLAPSE]) ? count($var) >= $options[self::COLLAPSE] : $options[self::COLLAPSE]);
219: $out = '<span class="tracy-toggle' . ($collapsed ? ' tracy-collapsed' : '') . '">'
220: . $out . count($var) . ")</span>\n<div" . ($collapsed ? ' class="tracy-collapsed"' : '') . '>';
221: $var[$marker] = true;
222: foreach ($var as $k => &$v) {
223: if ($k !== $marker) {
224: $hide = is_string($k) && isset($options[self::KEYS_TO_HIDE][strtolower($k)]) ? self::HIDDEN_VALUE : null;
225: $k = is_int($k) || preg_match('#^\w{1,50}\z#', $k) ? $k : '"' . Helpers::escapeHtml(self::encodeString($k, $options[self::TRUNCATE])) . '"';
226: $out .= '<span class="tracy-dump-indent"> ' . str_repeat('| ', $level) . '</span>'
227: . '<span class="tracy-dump-key">' . $k . '</span> => '
228: . ($hide ? self::dumpString($hide, $options) : self::dumpVar($v, $options, $level + 1));
229: }
230: }
231: unset($var[$marker]);
232: return $out . '</div>';
233:
234: } else {
235: return $out . count($var) . ") [ ... ]\n";
236: }
237: }
238:
239:
240: private static function dumpObject(&$var, $options, $level)
241: {
242: $fields = self::exportObject($var, $options[self::OBJECT_EXPORTERS], $options[self::DEBUGINFO]);
243:
244: $editorAttributes = '';
245: if ($options[self::LOCATION] & self::LOCATION_CLASS) {
246: $rc = $var instanceof \Closure ? new \ReflectionFunction($var) : new \ReflectionClass($var);
247: $editor = Helpers::editorUri($rc->getFileName(), $rc->getStartLine());
248: if ($editor) {
249: $editorAttributes = Helpers::formatHtml(
250: ' title="Declared in file % on line %" data-tracy-href="%"',
251: $rc->getFileName(),
252: $rc->getStartLine(),
253: $editor
254: );
255: }
256: }
257: $out = '<span class="tracy-dump-object"' . $editorAttributes . '>'
258: . Helpers::escapeHtml(Helpers::getClass($var))
259: . '</span> <span class="tracy-dump-hash">#' . substr(md5(spl_object_hash($var)), 0, 4) . '</span>';
260:
261: static $list = [];
262:
263: if (empty($fields)) {
264: return $out . "\n";
265:
266: } elseif (in_array($var, $list, true)) {
267: return $out . " { <i>RECURSION</i> }\n";
268:
269: } elseif (!$options[self::DEPTH] || $level < $options[self::DEPTH] || $var instanceof \Closure) {
270: $collapsed = $level ? count($fields) >= $options[self::COLLAPSE_COUNT]
271: : (is_int($options[self::COLLAPSE]) ? count($fields) >= $options[self::COLLAPSE] : $options[self::COLLAPSE]);
272: $out = '<span class="tracy-toggle' . ($collapsed ? ' tracy-collapsed' : '') . '">'
273: . $out . "</span>\n<div" . ($collapsed ? ' class="tracy-collapsed"' : '') . '>';
274: $list[] = $var;
275: foreach ($fields as $k => &$v) {
276: $vis = '';
277: if (isset($k[0]) && $k[0] === "\x00") {
278: $vis = ' <span class="tracy-dump-visibility">' . ($k[1] === '*' ? 'protected' : 'private') . '</span>';
279: $k = substr($k, strrpos($k, "\x00") + 1);
280: }
281: $hide = is_string($k) && isset($options[self::KEYS_TO_HIDE][strtolower($k)]) ? self::HIDDEN_VALUE : null;
282: $k = is_int($k) || preg_match('#^\w{1,50}\z#', $k) ? $k : '"' . Helpers::escapeHtml(self::encodeString($k, $options[self::TRUNCATE])) . '"';
283: $out .= '<span class="tracy-dump-indent"> ' . str_repeat('| ', $level) . '</span>'
284: . '<span class="tracy-dump-key">' . $k . "</span>$vis => "
285: . ($hide ? self::dumpString($hide, $options) : self::dumpVar($v, $options, $level + 1));
286: }
287: array_pop($list);
288: return $out . '</div>';
289:
290: } else {
291: return $out . " { ... }\n";
292: }
293: }
294:
295:
296: private static function dumpResource(&$var, $options, $level)
297: {
298: $type = get_resource_type($var);
299: $out = '<span class="tracy-dump-resource">' . Helpers::escapeHtml($type) . ' resource</span> '
300: . '<span class="tracy-dump-hash">#' . (int) $var . '</span>';
301: if (isset(self::$resources[$type])) {
302: $out = "<span class=\"tracy-toggle tracy-collapsed\">$out</span>\n<div class=\"tracy-collapsed\">";
303: foreach (call_user_func(self::$resources[$type], $var) as $k => $v) {
304: $out .= '<span class="tracy-dump-indent"> ' . str_repeat('| ', $level) . '</span>'
305: . '<span class="tracy-dump-key">' . Helpers::escapeHtml($k) . '</span> => ' . self::dumpVar($v, $options, $level + 1);
306: }
307: return $out . '</div>';
308: }
309: return "$out\n";
310: }
311:
312:
313: 314: 315:
316: private static function toJson(&$var, $options, $level = 0)
317: {
318: if (is_bool($var) || $var === null || is_int($var)) {
319: return $var;
320:
321: } elseif (is_float($var)) {
322: return is_finite($var)
323: ? (strpos($tmp = json_encode($var), '.') ? $var : ['number' => "$tmp.0"])
324: : ['type' => (string) $var];
325:
326: } elseif (is_string($var)) {
327: return self::encodeString($var, $options[self::TRUNCATE]);
328:
329: } elseif (is_array($var)) {
330: static $marker;
331: if ($marker === null) {
332: $marker = uniqid("\x00", true);
333: }
334: if (isset($var[$marker]) || $level >= $options[self::DEPTH]) {
335: return [null];
336: }
337: $res = [];
338: $var[$marker] = true;
339: foreach ($var as $k => &$v) {
340: if ($k !== $marker) {
341: $hide = is_string($k) && isset($options[self::KEYS_TO_HIDE][strtolower($k)]);
342: $k = is_int($k) || preg_match('#^\w{1,50}\z#', $k) ? $k : '"' . self::encodeString($k, $options[self::TRUNCATE]) . '"';
343: $res[] = [$k, $hide ? self::HIDDEN_VALUE : self::toJson($v, $options, $level + 1)];
344: }
345: }
346: unset($var[$marker]);
347: return $res;
348:
349: } elseif (is_object($var)) {
350: $obj = &self::$liveStorage[spl_object_hash($var)];
351: if ($obj && $obj['level'] <= $level) {
352: return ['object' => $obj['id']];
353: }
354:
355: $editorInfo = null;
356: if ($options[self::LOCATION] & self::LOCATION_CLASS) {
357: $rc = $var instanceof \Closure ? new \ReflectionFunction($var) : new \ReflectionClass($var);
358: $editor = Helpers::editorUri($rc->getFileName(), $rc->getStartLine());
359: $editorInfo = $editor ? ['file' => $rc->getFileName(), 'line' => $rc->getStartLine(), 'url' => $editor] : null;
360: }
361: static $counter = 1;
362: $obj = $obj ?: [
363: 'id' => self::$livePrefix . '0' . $counter++,
364: 'name' => Helpers::getClass($var),
365: 'editor' => $editorInfo,
366: 'level' => $level,
367: 'object' => $var,
368: ];
369:
370: if ($level < $options[self::DEPTH] || !$options[self::DEPTH]) {
371: $obj['level'] = $level;
372: $obj['items'] = [];
373:
374: foreach (self::exportObject($var, $options[self::OBJECT_EXPORTERS], $options[self::DEBUGINFO]) as $k => $v) {
375: $vis = 0;
376: if (isset($k[0]) && $k[0] === "\x00") {
377: $vis = $k[1] === '*' ? 1 : 2;
378: $k = substr($k, strrpos($k, "\x00") + 1);
379: }
380: $hide = is_string($k) && isset($options[self::KEYS_TO_HIDE][strtolower($k)]);
381: $k = is_int($k) || preg_match('#^\w{1,50}\z#', $k) ? $k : '"' . self::encodeString($k, $options[self::TRUNCATE]) . '"';
382: $obj['items'][] = [$k, $hide ? self::HIDDEN_VALUE : self::toJson($v, $options, $level + 1), $vis];
383: }
384: }
385: return ['object' => $obj['id']];
386:
387: } elseif (is_resource($var)) {
388: $obj = &self::$liveStorage[(string) $var];
389: if (!$obj) {
390: $type = get_resource_type($var);
391: $obj = ['id' => self::$livePrefix . (int) $var, 'name' => $type . ' resource'];
392: if (isset(self::$resources[$type])) {
393: foreach (call_user_func(self::$resources[$type], $var) as $k => $v) {
394: $obj['items'][] = [$k, self::toJson($v, $options, $level + 1)];
395: }
396: }
397: }
398: return ['resource' => $obj['id']];
399:
400: } else {
401: return ['type' => 'unknown type'];
402: }
403: }
404:
405:
406:
407: public static function fetchLiveData()
408: {
409: $res = [];
410: foreach (self::$liveStorage as $obj) {
411: $id = $obj['id'];
412: unset($obj['level'], $obj['object'], $obj['id']);
413: $res[$id] = $obj;
414: }
415: self::$liveStorage = [];
416: return $res;
417: }
418:
419:
420: 421: 422: 423:
424: public static function encodeString($s, $maxLength = null)
425: {
426: static $table;
427: if ($table === null) {
428: foreach (array_merge(range("\x00", "\x1F"), range("\x7F", "\xFF")) as $ch) {
429: $table[$ch] = '\x' . str_pad(dechex(ord($ch)), 2, '0', STR_PAD_LEFT);
430: }
431: $table['\\'] = '\\\\';
432: $table["\r"] = '\r';
433: $table["\n"] = '\n';
434: $table["\t"] = '\t';
435: }
436:
437: if ($maxLength && strlen($s) > $maxLength) {
438: if (function_exists('mb_substr')) {
439: $s = mb_substr($tmp = $s, 0, $maxLength, 'UTF-8');
440: $shortened = $s !== $tmp;
441: } else {
442: $i = $len = 0;
443: $maxI = $maxLength * 4;
444: do {
445: if (($s[$i] < "\x80" || $s[$i] >= "\xC0") && (++$len > $maxLength) || $i >= $maxI) {
446: $s = substr($s, 0, $i);
447: $shortened = true;
448: break;
449: }
450: } while (isset($s[++$i]));
451: }
452: }
453:
454: if (preg_match('#[^\x09\x0A\x0D\x20-\x7E\xA0-\x{10FFFF}]#u', $s) || preg_last_error()) {
455: if ($maxLength && strlen($s) > $maxLength) {
456: $s = substr($s, 0, $maxLength);
457: $shortened = true;
458: }
459: $s = strtr($s, $table);
460: }
461:
462: return $s . (empty($shortened) ? '' : ' ... ');
463: }
464:
465:
466: 467: 468:
469: private static function exportObject($obj, array $exporters, $useDebugInfo)
470: {
471: foreach ($exporters as $type => $dumper) {
472: if (!$type || $obj instanceof $type) {
473: return call_user_func($dumper, $obj);
474: }
475: }
476:
477: if ($useDebugInfo && method_exists($obj, '__debugInfo')) {
478: return $obj->__debugInfo();
479: }
480:
481: return (array) $obj;
482: }
483:
484:
485: 486: 487:
488: private static function exportClosure(\Closure $obj)
489: {
490: $rc = new \ReflectionFunction($obj);
491: $res = [];
492: foreach ($rc->getParameters() as $param) {
493: $res[] = '$' . $param->getName();
494: }
495: return [
496: 'file' => $rc->getFileName(),
497: 'line' => $rc->getStartLine(),
498: 'variables' => $rc->getStaticVariables(),
499: 'parameters' => implode(', ', $res),
500: ];
501: }
502:
503:
504: 505: 506:
507: private static function exportSplFileInfo(\SplFileInfo $obj)
508: {
509: return ['path' => $obj->getPathname()];
510: }
511:
512:
513: 514: 515:
516: private static function exportSplObjectStorage(\SplObjectStorage $obj)
517: {
518: $res = [];
519: foreach (clone $obj as $item) {
520: $res[] = ['object' => $item, 'data' => $obj[$item]];
521: }
522: return $res;
523: }
524:
525:
526: 527: 528:
529: private static function exportPhpIncompleteClass(\__PHP_Incomplete_Class $obj)
530: {
531: $info = ['className' => null, 'private' => [], 'protected' => [], 'public' => []];
532: foreach ((array) $obj as $name => $value) {
533: if ($name === '__PHP_Incomplete_Class_Name') {
534: $info['className'] = $value;
535: } elseif (preg_match('#^\x0\*\x0(.+)\z#', $name, $m)) {
536: $info['protected'][$m[1]] = $value;
537: } elseif (preg_match('#^\x0(.+)\x0(.+)\z#', $name, $m)) {
538: $info['private'][$m[1] . '::$' . $m[2]] = $value;
539: } else {
540: $info['public'][$name] = $value;
541: }
542: }
543: return $info;
544: }
545:
546:
547: 548: 549: 550:
551: private static function findLocation()
552: {
553: foreach (debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS) as $item) {
554: if (isset($item['class']) && $item['class'] === __CLASS__) {
555: $location = $item;
556: continue;
557: } elseif (isset($item['function'])) {
558: try {
559: $reflection = isset($item['class'])
560: ? new \ReflectionMethod($item['class'], $item['function'])
561: : new \ReflectionFunction($item['function']);
562: if ($reflection->isInternal() || preg_match('#\s@tracySkipLocation\s#', (string) $reflection->getDocComment())) {
563: $location = $item;
564: continue;
565: }
566: } catch (\ReflectionException $e) {
567: }
568: }
569: break;
570: }
571:
572: if (isset($location['file'], $location['line']) && is_file($location['file'])) {
573: $lines = file($location['file']);
574: $line = $lines[$location['line'] - 1];
575: return [
576: $location['file'],
577: $location['line'],
578: trim(preg_match('#\w*dump(er::\w+)?\(.*\)#i', $line, $m) ? $m[0] : $line),
579: ];
580: }
581: }
582:
583:
584: 585: 586:
587: private static function detectColors()
588: {
589: return self::$terminalColors &&
590: (getenv('ConEmuANSI') === 'ON'
591: || getenv('ANSICON') !== false
592: || getenv('term') === 'xterm-256color'
593: || (defined('STDOUT') && function_exists('posix_isatty') && posix_isatty(STDOUT)));
594: }
595: }
596: