1: <?php
2:
3: 4: 5: 6:
7:
8: namespace Nette\Diagnostics;
9:
10: use Nette;
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="nette-dump"'
80: . ($file ? ' title="' . htmlspecialchars("$code\nin file $file on line $line", ENT_IGNORE | ENT_QUOTES) . '">' : '>')
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="nette-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=\"nette-dump-null\">NULL</span>\n";
129: }
130:
131:
132: private static function dumpBoolean(& $var)
133: {
134: return '<span class="nette-dump-bool">' . ($var ? 'TRUE' : 'FALSE') . "</span>\n";
135: }
136:
137:
138: private static function dumpInteger(& $var)
139: {
140: return "<span class=\"nette-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=\"nette-dump-number\">$var</span>\n";
150: }
151:
152:
153: private static function dumpString(& $var, $options)
154: {
155: return '<span class="nette-dump-string">'
156: . self::encodeString($options[self::TRUNCATE] && strlen($var) > $options[self::TRUNCATE] ? substr($var, 0, $options[self::TRUNCATE]) . ' ... ' : $var)
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="nette-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="nette-toggle' . ($collapsed ? '-collapsed' : '') . '">' . $out . count($var) . ")</span>\n<div" . ($collapsed ? ' class="nette-collapsed"' : '') . '>';
179: $var[$marker] = TRUE;
180: foreach ($var as $k => & $v) {
181: if ($k !== $marker) {
182: $out .= '<span class="nette-dump-indent"> ' . str_repeat('| ', $level) . '</span>'
183: . '<span class="nette-dump-key">' . (preg_match('#^\w+\z#', $k) ? $k : self::encodeString($k)) . '</span> => '
184: . self::dumpVar($v, $options, $level + 1);
185: }
186: }
187: unset($var[$marker]);
188: return $out . '</div>';
189:
190: } else {
191: return $out . count($var) . ") [ ... ]\n";
192: }
193: }
194:
195:
196: private static function dumpObject(& $var, $options, $level)
197: {
198: if ($var instanceof \Closure) {
199: $rc = new \ReflectionFunction($var);
200: $fields = array();
201: foreach ($rc->getParameters() as $param) {
202: $fields[] = '$' . $param->getName();
203: }
204: $fields = array(
205: 'file' => $rc->getFileName(), 'line' => $rc->getStartLine(),
206: 'variables' => $rc->getStaticVariables(), 'parameters' => implode(', ', $fields)
207: );
208: } elseif ($var instanceof \SplFileInfo) {
209: $fields = array('path' => $var->getPathname());
210: } elseif ($var instanceof \SplObjectStorage) {
211: $fields = array();
212: foreach (clone $var as $obj) {
213: $fields[] = array('object' => $obj, 'data' => $var[$obj]);
214: }
215: } else {
216: $fields = (array) $var;
217: }
218:
219: static $list = array();
220: $rc = $var instanceof \Closure ? new \ReflectionFunction($var) : new \ReflectionClass($var);
221: $out = '<span class="nette-dump-object"'
222: . ($options[self::LOCATION] && $rc->getFileName() ? ' data-nette-href="' . htmlspecialchars(strtr(Debugger::$editor, array('%file' => rawurlencode($rc->getFileName()), '%line' => $rc->getStartLine()))) . '"' : '')
223: . '>' . get_class($var) . '</span> <span class="nette-dump-hash">#' . substr(md5(spl_object_hash($var)), 0, 4) . '</span>';
224:
225: if (empty($fields)) {
226: return $out . "\n";
227:
228: } elseif (in_array($var, $list, TRUE)) {
229: return $out . " { <i>RECURSION</i> }\n";
230:
231: } elseif (!$options[self::DEPTH] || $level < $options[self::DEPTH] || $var instanceof \Closure) {
232: $collapsed = $level ? count($fields) >= $options[self::COLLAPSE_COUNT] : $options[self::COLLAPSE];
233: $out = '<span class="nette-toggle' . ($collapsed ? '-collapsed' : '') . '">' . $out . "</span>\n<div" . ($collapsed ? ' class="nette-collapsed"' : '') . '>';
234: $list[] = $var;
235: foreach ($fields as $k => & $v) {
236: $vis = '';
237: if ($k[0] === "\x00") {
238: $vis = ' <span class="nette-dump-visibility">' . ($k[1] === '*' ? 'protected' : 'private') . '</span>';
239: $k = substr($k, strrpos($k, "\x00") + 1);
240: }
241: $out .= '<span class="nette-dump-indent"> ' . str_repeat('| ', $level) . '</span>'
242: . '<span class="nette-dump-key">' . (preg_match('#^\w+\z#', $k) ? $k : self::encodeString($k)) . "</span>$vis => "
243: . self::dumpVar($v, $options, $level + 1);
244: }
245: array_pop($list);
246: return $out . '</div>';
247:
248: } else {
249: return $out . " { ... }\n";
250: }
251: }
252:
253:
254: private static function dumpResource(& $var, $options, $level)
255: {
256: $type = get_resource_type($var);
257: $out = '<span class="nette-dump-resource">' . htmlSpecialChars($type) . ' resource</span>';
258: if (isset(self::$resources[$type])) {
259: $out = "<span class=\"nette-toggle-collapsed\">$out</span>\n<div class=\"nette-collapsed\">";
260: foreach (call_user_func(self::$resources[$type], $var) as $k => $v) {
261: $out .= '<span class="nette-dump-indent"> ' . str_repeat('| ', $level) . '</span>'
262: . '<span class="nette-dump-key">' . htmlSpecialChars($k) . "</span> => " . self::dumpVar($v, $options, $level + 1);
263: }
264: return $out . '</div>';
265: }
266: return "$out\n";
267: }
268:
269:
270: private static function encodeString($s)
271: {
272: static $table;
273: if ($table === NULL) {
274: foreach (array_merge(range("\x00", "\x1F"), range("\x7F", "\xFF")) as $ch) {
275: $table[$ch] = '\x' . str_pad(dechex(ord($ch)), 2, '0', STR_PAD_LEFT);
276: }
277: $table["\\"] = '\\\\';
278: $table["\r"] = '\r';
279: $table["\n"] = '\n';
280: $table["\t"] = '\t';
281: }
282:
283: if (preg_match('#[^\x09\x0A\x0D\x20-\x7E\xA0-\x{10FFFF}]#u', $s) || preg_last_error()) {
284: $s = strtr($s, $table);
285: }
286: return '"' . htmlSpecialChars($s, ENT_NOQUOTES) . '"';
287: }
288:
289:
290: 291: 292: 293:
294: private static function findLocation()
295: {
296: foreach (debug_backtrace(PHP_VERSION_ID >= 50306 ? DEBUG_BACKTRACE_IGNORE_ARGS : FALSE) as $item) {
297: if (isset($item['class']) && $item['class'] === __CLASS__) {
298: $location = $item;
299: continue;
300: } elseif (isset($item['function'])) {
301: try {
302: $reflection = isset($item['class'])
303: ? new \ReflectionMethod($item['class'], $item['function'])
304: : new \ReflectionFunction($item['function']);
305: if (preg_match('#\s@tracySkipLocation\s#', $reflection->getDocComment())) {
306: $location = $item;
307: continue;
308: }
309: } catch (\ReflectionException $e) {}
310: }
311: break;
312: }
313:
314: if (isset($location['file'], $location['line']) && is_file($location['file'])) {
315: $lines = file($location['file']);
316: $line = $lines[$location['line'] - 1];
317: return array(
318: $location['file'],
319: $location['line'],
320: trim(preg_match('#\w*dump(er::\w+)?\(.*\)#i', $line, $m) ? $m[0] : $line)
321: );
322: }
323: }
324:
325:
326: 327: 328:
329: private static function detectColors()
330: {
331: return self::$terminalColors &&
332: (getenv('ConEmuANSI') === 'ON'
333: || getenv('ANSICON') !== FALSE
334: || (defined('STDOUT') && function_exists('posix_isatty') && posix_isatty(STDOUT)));
335: }
336:
337: }
338: