PageRenderTime 55ms CodeModel.GetById 20ms RepoModel.GetById 0ms app.codeStats 1ms

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