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