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:
26:
27: public static $terminalColors = array(
28: 'bool' => '1;33',
29: 'null' => '1;33',
30: 'number' => '1;32',
31: 'string' => '1;36',
32: 'array' => '1;31',
33: 'key' => '1;37',
34: 'object' => '1;31',
35: 'visibility' => '1;30',
36: 'resource' => '1;37',
37: 'indent' => '1;30',
38: );
39:
40:
41: public static $resources = array(
42: 'stream' => 'stream_get_meta_data',
43: 'stream-context' => 'stream_context_get_options',
44: 'curl' => 'curl_getinfo',
45: );
46:
47:
48: 49: 50: 51:
52: public static function dump($var, array $options = NULL)
53: {
54: if (PHP_SAPI !== 'cli' && !preg_match('#^Content-Type: (?!text/html)#im', implode("\n", headers_list()))) {
55: echo self::toHtml($var, $options);
56: } elseif (self::detectColors()) {
57: echo self::toTerminal($var, $options);
58: } else {
59: echo self::toText($var, $options);
60: }
61: return $var;
62: }
63:
64:
65: 66: 67: 68:
69: public static function toHtml($var, array $options = NULL)
70: {
71: $options = (array) $options + array(
72: self::DEPTH => 4,
73: self::TRUNCATE => 150,
74: self::COLLAPSE => FALSE,
75: self::COLLAPSE_COUNT => 7,
76: self::LOCATION => FALSE,
77: );
78: list($file, $line, $code) = $options[self::LOCATION] ? self::findLocation() : NULL;
79: return '<pre class="tracy-dump"'
80: . ($file ? Helpers::createHtml(' title="%in file % on line %" data-tracy-href="%"', "$code\n", $file, $line, Helpers::editorUri($file, $line)) . '>' : '>')
81: . self::dumpVar($var, $options)
82: . ($file ? '<small>in ' . Helpers::editorLink($file, $line) . '</small>' : '')
83: . "</pre>\n";
84: }
85:
86:
87: 88: 89: 90:
91: public static function toText($var, array $options = NULL)
92: {
93: return htmlspecialchars_decode(strip_tags(self::toHtml($var, $options)), ENT_QUOTES);
94: }
95:
96:
97: 98: 99: 100:
101: public static function toTerminal($var, array $options = NULL)
102: {
103: return htmlspecialchars_decode(strip_tags(preg_replace_callback('#<span class="tracy-dump-(\w+)">|</span>#', function ($m) {
104: return "\033[" . (isset($m[1], Dumper::$terminalColors[$m[1]]) ? Dumper::$terminalColors[$m[1]] : '0') . 'm';
105: }, self::toHtml($var, $options))), ENT_QUOTES);
106: }
107:
108:
109: 110: 111: 112: 113: 114: 115:
116: private static function dumpVar(& $var, array $options, $level = 0)
117: {
118: if (method_exists(__CLASS__, $m = 'dump' . gettype($var))) {
119: return self::$m($var, $options, $level);
120: } else {
121: return "<span>unknown type</span>\n";
122: }
123: }
124:
125:
126: private static function dumpNull()
127: {
128: return "<span class=\"tracy-dump-null\">NULL</span>\n";
129: }
130:
131:
132: private static function dumpBoolean(& $var)
133: {
134: return '<span class="tracy-dump-bool">' . ($var ? 'TRUE' : 'FALSE') . "</span>\n";
135: }
136:
137:
138: private static function dumpInteger(& $var)
139: {
140: return "<span class=\"tracy-dump-number\">$var</span>\n";
141: }
142:
143:
144: private static function dumpDouble(& $var)
145: {
146: $var = is_finite($var)
147: ? ($tmp = json_encode($var)) . (strpos($tmp, '.') === FALSE ? '.0' : '')
148: : var_export($var, TRUE);
149: return "<span class=\"tracy-dump-number\">$var</span>\n";
150: }
151:
152:
153: private static function dumpString(& $var, $options)
154: {
155: return '<span class="tracy-dump-string">"'
156: . htmlspecialchars(self::encodeString($var, $options[self::TRUNCATE]), ENT_NOQUOTES, 'UTF-8')
157: . '"</span>' . (strlen($var) > 1 ? ' (' . strlen($var) . ')' : '') . "\n";
158: }
159:
160:
161: private static function dumpArray(& $var, $options, $level)
162: {
163: static $marker;
164: if ($marker === NULL) {
165: $marker = uniqid("\x00", TRUE);
166: }
167:
168: $out = '<span class="tracy-dump-array">array</span> (';
169:
170: if (empty($var)) {
171: return $out . ")\n";
172:
173: } elseif (isset($var[$marker])) {
174: return $out . (count($var) - 1) . ") [ <i>RECURSION</i> ]\n";
175:
176: } elseif (!$options[self::DEPTH] || $level < $options[self::DEPTH]) {
177: $collapsed = $level ? count($var) >= $options[self::COLLAPSE_COUNT] : $options[self::COLLAPSE];
178: $out = '<span class="tracy-toggle' . ($collapsed ? ' tracy-collapsed' : '') . '">'
179: . $out . count($var) . ")</span>\n<div" . ($collapsed ? ' class="tracy-collapsed"' : '') . '>';
180: $var[$marker] = TRUE;
181: foreach ($var as $k => & $v) {
182: if ($k !== $marker) {
183: $k = preg_match('#^\w{1,50}\z#', $k) ? $k : '"' . htmlspecialchars(self::encodeString($k, $options[self::TRUNCATE]), ENT_NOQUOTES, 'UTF-8') . '"';
184: $out .= '<span class="tracy-dump-indent"> ' . str_repeat('| ', $level) . '</span>'
185: . '<span class="tracy-dump-key">' . $k . '</span> => '
186: . self::dumpVar($v, $options, $level + 1);
187: }
188: }
189: unset($var[$marker]);
190: return $out . '</div>';
191:
192: } else {
193: return $out . count($var) . ") [ ... ]\n";
194: }
195: }
196:
197:
198: private static function dumpObject(& $var, $options, $level)
199: {
200: if ($var instanceof \Closure) {
201: $rc = new \ReflectionFunction($var);
202: $fields = array();
203: foreach ($rc->getParameters() as $param) {
204: $fields[] = '$' . $param->getName();
205: }
206: $fields = array(
207: 'file' => $rc->getFileName(), 'line' => $rc->getStartLine(),
208: 'variables' => $rc->getStaticVariables(), 'parameters' => implode(', ', $fields)
209: );
210: } elseif ($var instanceof \SplFileInfo) {
211: $fields = array('path' => $var->getPathname());
212: } elseif ($var instanceof \SplObjectStorage) {
213: $fields = array();
214: foreach (clone $var as $obj) {
215: $fields[] = array('object' => $obj, 'data' => $var[$obj]);
216: }
217: } else {
218: $fields = (array) $var;
219: }
220:
221: static $list = array();
222: $rc = $var instanceof \Closure ? new \ReflectionFunction($var) : new \ReflectionClass($var);
223: $out = '<span class="tracy-dump-object"'
224: . ($options[self::LOCATION] && ($editor = Helpers::editorUri($rc->getFileName(), $rc->getStartLine())) ? ' data-tracy-href="' . htmlspecialchars($editor) . '"' : '')
225: . '>' . htmlspecialchars(Helpers::getClass($var)) . '</span> <span class="tracy-dump-hash">#' . substr(md5(spl_object_hash($var)), 0, 4) . '</span>';
226:
227: if (empty($fields)) {
228: return $out . "\n";
229:
230: } elseif (in_array($var, $list, TRUE)) {
231: return $out . " { <i>RECURSION</i> }\n";
232:
233: } elseif (!$options[self::DEPTH] || $level < $options[self::DEPTH] || $var instanceof \Closure) {
234: $collapsed = $level ? count($fields) >= $options[self::COLLAPSE_COUNT] : $options[self::COLLAPSE];
235: $out = '<span class="tracy-toggle' . ($collapsed ? ' tracy-collapsed' : '') . '">'
236: . $out . "</span>\n<div" . ($collapsed ? ' class="tracy-collapsed"' : '') . '>';
237: $list[] = $var;
238: foreach ($fields as $k => & $v) {
239: $vis = '';
240: if ($k[0] === "\x00") {
241: $vis = ' <span class="tracy-dump-visibility">' . ($k[1] === '*' ? 'protected' : 'private') . '</span>';
242: $k = substr($k, strrpos($k, "\x00") + 1);
243: }
244: $k = preg_match('#^\w{1,50}\z#', $k) ? $k : '"' . htmlspecialchars(self::encodeString($k, $options[self::TRUNCATE]), ENT_NOQUOTES, 'UTF-8') . '"';
245: $out .= '<span class="tracy-dump-indent"> ' . str_repeat('| ', $level) . '</span>'
246: . '<span class="tracy-dump-key">' . $k . "</span>$vis => "
247: . self::dumpVar($v, $options, $level + 1);
248: }
249: array_pop($list);
250: return $out . '</div>';
251:
252: } else {
253: return $out . " { ... }\n";
254: }
255: }
256:
257:
258: private static function dumpResource(& $var, $options, $level)
259: {
260: $type = get_resource_type($var);
261: $out = '<span class="tracy-dump-resource">' . htmlSpecialChars($type, ENT_IGNORE, 'UTF-8') . ' resource</span> '
262: . '<span class="tracy-dump-hash">#' . intval($var) . '</span>';
263: if (isset(self::$resources[$type])) {
264: $out = "<span class=\"tracy-toggle tracy-collapsed\">$out</span>\n<div class=\"tracy-collapsed\">";
265: foreach (call_user_func(self::$resources[$type], $var) as $k => $v) {
266: $out .= '<span class="tracy-dump-indent"> ' . str_repeat('| ', $level) . '</span>'
267: . '<span class="tracy-dump-key">' . htmlSpecialChars($k, ENT_IGNORE, 'UTF-8') . '</span> => ' . self::dumpVar($v, $options, $level + 1);
268: }
269: return $out . '</div>';
270: }
271: return "$out\n";
272: }
273:
274:
275: 276: 277: 278:
279: public static function encodeString($s, $maxLength = NULL)
280: {
281: static $table;
282: if ($table === NULL) {
283: foreach (array_merge(range("\x00", "\x1F"), range("\x7F", "\xFF")) as $ch) {
284: $table[$ch] = '\x' . str_pad(dechex(ord($ch)), 2, '0', STR_PAD_LEFT);
285: }
286: $table['\\'] = '\\\\';
287: $table["\r"] = '\r';
288: $table["\n"] = '\n';
289: $table["\t"] = '\t';
290: }
291:
292: if (preg_match('#[^\x09\x0A\x0D\x20-\x7E\xA0-\x{10FFFF}]#u', $s) || preg_last_error()) {
293: if ($maxLength && strlen($s) > $maxLength) {
294: $s = substr($s, 0, $maxLength) . ' ... ';
295: }
296: $s = strtr($s, $table);
297: } elseif ($maxLength && strlen(utf8_decode($s)) > $maxLength) {
298: $s = iconv_substr($s, 0, $maxLength, 'UTF-8') . ' ... ';
299: }
300:
301: return $s;
302: }
303:
304:
305: 306: 307: 308:
309: private static function findLocation()
310: {
311: foreach (debug_backtrace(PHP_VERSION_ID >= 50306 ? DEBUG_BACKTRACE_IGNORE_ARGS : FALSE) as $item) {
312: if (isset($item['class']) && $item['class'] === __CLASS__) {
313: $location = $item;
314: continue;
315: } elseif (isset($item['function'])) {
316: try {
317: $reflection = isset($item['class'])
318: ? new \ReflectionMethod($item['class'], $item['function'])
319: : new \ReflectionFunction($item['function']);
320: if ($reflection->isInternal() || preg_match('#\s@tracySkipLocation\s#', $reflection->getDocComment())) {
321: $location = $item;
322: continue;
323: }
324: } catch (\ReflectionException $e) {
325: }
326: }
327: break;
328: }
329:
330: if (isset($location['file'], $location['line']) && is_file($location['file'])) {
331: $lines = file($location['file']);
332: $line = $lines[$location['line'] - 1];
333: return array(
334: $location['file'],
335: $location['line'],
336: trim(preg_match('#\w*dump(er::\w+)?\(.*\)#i', $line, $m) ? $m[0] : $line),
337: );
338: }
339: }
340:
341:
342: 343: 344:
345: private static function detectColors()
346: {
347: return self::$terminalColors &&
348: (getenv('ConEmuANSI') === 'ON'
349: || getenv('ANSICON') !== FALSE
350: || (defined('STDOUT') && function_exists('posix_isatty') && posix_isatty(STDOUT)));
351: }
352:
353: }
354: