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