PageRenderTime 522ms CodeModel.GetById 91ms app.highlight 311ms RepoModel.GetById 52ms app.codeStats 2ms

/lib/external/tracy/src/Tracy/Dumper.php

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