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: . '>' . htmlspecialchars(Helpers::getClass($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)) {
299: return $var;
300:
301: } elseif (is_float($var)) {
302: return is_finite($var)
303: ? (strpos($tmp = json_encode($var), '.') ? $var : array('number' => "$tmp.0"))
304: : array('type' => (string) $var);
305:
306: } elseif (is_string($var)) {
307: return self::encodeString($var, $options[self::TRUNCATE]);
308:
309: } elseif (is_array($var)) {
310: static $marker;
311: if ($marker === NULL) {
312: $marker = uniqid("\x00", TRUE);
313: }
314: if (isset($var[$marker]) || $level >= $options[self::DEPTH]) {
315: return array(NULL);
316: }
317: $res = array();
318: $var[$marker] = TRUE;
319: foreach ($var as $k => & $v) {
320: if ($k !== $marker) {
321: $k = preg_match('#^\w{1,50}\z#', $k) ? $k : '"' . self::encodeString($k, $options[self::TRUNCATE]) . '"';
322: $res[] = array($k, self::toJson($v, $options, $level + 1));
323: }
324: }
325: unset($var[$marker]);
326: return $res;
327:
328: } elseif (is_object($var)) {
329: $obj = & self::$liveStorage[spl_object_hash($var)];
330: if ($obj && $obj['level'] <= $level) {
331: return array('object' => $obj['id']);
332: }
333:
334: if ($options[self::LOCATION] & self::LOCATION_CLASS) {
335: $rc = $var instanceof \Closure ? new \ReflectionFunction($var) : new \ReflectionClass($var);
336: $editor = Helpers::editorUri($rc->getFileName(), $rc->getStartLine());
337: }
338: static $counter = 1;
339: $obj = $obj ?: array(
340: 'id' => self::$livePrefix . '0' . $counter++,
341: 'name' => Helpers::getClass($var),
342: 'editor' => empty($editor) ? NULL : array('file' => $rc->getFileName(), 'line' => $rc->getStartLine(), 'url' => $editor),
343: 'level' => $level,
344: 'object' => $var,
345: );
346:
347: if ($level < $options[self::DEPTH] || !$options[self::DEPTH]) {
348: $obj['level'] = $level;
349: $obj['items'] = array();
350:
351: foreach (self::exportObject($var, $options[self::OBJECT_EXPORTERS]) as $k => $v) {
352: $vis = 0;
353: if ($k[0] === "\x00") {
354: $vis = $k[1] === '*' ? 1 : 2;
355: $k = substr($k, strrpos($k, "\x00") + 1);
356: }
357: $k = preg_match('#^\w{1,50}\z#', $k) ? $k : '"' . self::encodeString($k, $options[self::TRUNCATE]) . '"';
358: $obj['items'][] = array($k, self::toJson($v, $options, $level + 1), $vis);
359: }
360: }
361: return array('object' => $obj['id']);
362:
363: } elseif (is_resource($var)) {
364: $obj = & self::$liveStorage[(string) $var];
365: if (!$obj) {
366: $type = get_resource_type($var);
367: $obj = array('id' => self::$livePrefix . (int) $var, 'name' => $type . ' resource');
368: if (isset(self::$resources[$type])) {
369: foreach (call_user_func(self::$resources[$type], $var) as $k => $v) {
370: $obj['items'][] = array($k, self::toJson($v, $options, $level + 1));
371: }
372: }
373: }
374: return array('resource' => $obj['id']);
375:
376: } else {
377: return array('type' => 'unknown type');
378: }
379: }
380:
381:
382:
383: public static function fetchLiveData()
384: {
385: $res = array();
386: foreach (self::$liveStorage as $obj) {
387: $id = $obj['id'];
388: unset($obj['level'], $obj['object'], $obj['id']);
389: $res[$id] = $obj;
390: }
391: self::$liveStorage = array();
392: return $res;
393: }
394:
395:
396: 397: 398: 399:
400: public static function encodeString($s, $maxLength = NULL)
401: {
402: static $table;
403: if ($table === NULL) {
404: foreach (array_merge(range("\x00", "\x1F"), range("\x7F", "\xFF")) as $ch) {
405: $table[$ch] = '\x' . str_pad(dechex(ord($ch)), 2, '0', STR_PAD_LEFT);
406: }
407: $table['\\'] = '\\\\';
408: $table["\r"] = '\r';
409: $table["\n"] = '\n';
410: $table["\t"] = '\t';
411: }
412:
413: if (preg_match('#[^\x09\x0A\x0D\x20-\x7E\xA0-\x{10FFFF}]#u', $s) || preg_last_error()) {
414: if ($maxLength && strlen($s) > $maxLength) {
415: $s = substr($s, 0, $maxLength) . ' ... ';
416: }
417: $s = strtr($s, $table);
418: } elseif ($maxLength && strlen(utf8_decode($s)) > $maxLength) {
419: $s = iconv_substr($s, 0, $maxLength, 'UTF-8') . ' ... ';
420: }
421:
422: return $s;
423: }
424:
425:
426: 427: 428:
429: private static function exportObject($obj, array $exporters)
430: {
431: foreach ($exporters as $type => $dumper) {
432: if ($obj instanceof $type) {
433: return call_user_func($dumper, $obj);
434: }
435: }
436: return (array) $obj;
437: }
438:
439:
440: 441: 442:
443: private static function exportClosure(\Closure $obj)
444: {
445: $rc = new \ReflectionFunction($obj);
446: $res = array();
447: foreach ($rc->getParameters() as $param) {
448: $res[] = '$' . $param->getName();
449: }
450: return array(
451: 'file' => $rc->getFileName(),
452: 'line' => $rc->getStartLine(),
453: 'variables' => $rc->getStaticVariables(),
454: 'parameters' => implode(', ', $res),
455: );
456: }
457:
458:
459: 460: 461:
462: private static function exportSplFileInfo(\SplFileInfo $obj)
463: {
464: return array('path' => $obj->getPathname());
465: }
466:
467:
468: 469: 470:
471: private static function exportSplObjectStorage(\SplObjectStorage $obj)
472: {
473: $res = array();
474: foreach (clone $obj as $item) {
475: $res[] = array('object' => $item, 'data' => $obj[$item]);
476: }
477: return $res;
478: }
479:
480:
481: 482: 483:
484: private static function exportPhpIncompleteClass(\__PHP_Incomplete_Class $obj)
485: {
486: $info = array('className' => NULL, 'private' => array(), 'protected' => array(), 'public' => array());
487: foreach ((array) $obj as $name => $value) {
488: if ($name === '__PHP_Incomplete_Class_Name') {
489: $info['className'] = $value;
490: } elseif (preg_match('#^\x0\*\x0(.+)\z#', $name, $m)) {
491: $info['protected'][$m[1]] = $value;
492: } elseif (preg_match('#^\x0(.+)\x0(.+)\z#', $name, $m)) {
493: $info['private'][$m[1] . '::$' . $m[2]] = $value;
494: } else {
495: $info['public'][$name] = $value;
496: }
497: }
498: return $info;
499: }
500:
501:
502: 503: 504: 505:
506: private static function findLocation()
507: {
508: foreach (debug_backtrace(PHP_VERSION_ID >= 50306 ? DEBUG_BACKTRACE_IGNORE_ARGS : FALSE) as $item) {
509: if (isset($item['class']) && $item['class'] === __CLASS__) {
510: $location = $item;
511: continue;
512: } elseif (isset($item['function'])) {
513: try {
514: $reflection = isset($item['class'])
515: ? new \ReflectionMethod($item['class'], $item['function'])
516: : new \ReflectionFunction($item['function']);
517: if ($reflection->isInternal() || preg_match('#\s@tracySkipLocation\s#', $reflection->getDocComment())) {
518: $location = $item;
519: continue;
520: }
521: } catch (\ReflectionException $e) {
522: }
523: }
524: break;
525: }
526:
527: if (isset($location['file'], $location['line']) && is_file($location['file'])) {
528: $lines = file($location['file']);
529: $line = $lines[$location['line'] - 1];
530: return array(
531: $location['file'],
532: $location['line'],
533: trim(preg_match('#\w*dump(er::\w+)?\(.*\)#i', $line, $m) ? $m[0] : $line),
534: );
535: }
536: }
537:
538:
539: 540: 541:
542: private static function detectColors()
543: {
544: return self::$terminalColors &&
545: (getenv('ConEmuANSI') === 'ON'
546: || getenv('ANSICON') !== FALSE
547: || (defined('STDOUT') && function_exists('posix_isatty') && posix_isatty(STDOUT)));
548: }
549:
550: }
551: