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