Debug.php 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460
  1. <?php
  2. /**
  3. * Contains debugging and dumping tools.
  4. *
  5. * @package KO7
  6. * @category Base
  7. *
  8. * @copyright (c) 2007-2016 Kohana Team
  9. * @copyright (c) since 2016 Koseven Team
  10. * @license https://koseven.dev/LICENSE
  11. */
  12. class KO7_Debug {
  13. /**
  14. * Returns an HTML string of debugging information about any number of
  15. * variables wrapped in a "pre" tag:
  16. *
  17. * // Displays the type and value of each variable
  18. * echo Debug::vars($foo, $bar, $baz);
  19. *
  20. * @param mixed $var,... variable to debug
  21. * @return string
  22. */
  23. public static function vars()
  24. {
  25. if (func_num_args() === 0)
  26. return;
  27. // Get all passed variables
  28. $variables = func_get_args();
  29. $output = [];
  30. foreach ($variables as $var)
  31. {
  32. $output[] = Debug::_dump($var, 1024);
  33. }
  34. return '<pre class="debug">'.implode("\n", $output).'</pre>';
  35. }
  36. /**
  37. * Returns an HTML string of information about a single variable.
  38. *
  39. * Borrows heavily on concepts from the Debug class of [Nette](https://nette.org).
  40. *
  41. * @param mixed $value variable to dump
  42. * @param integer $length maximum length of strings
  43. * @param integer $level_recursion recursion limit
  44. * @return string
  45. */
  46. public static function dump($value, $length = 128, $level_recursion = 10)
  47. {
  48. return Debug::_dump($value, $length, $level_recursion);
  49. }
  50. /**
  51. * Helper for Debug::dump(), handles recursion in arrays and objects.
  52. *
  53. * @param mixed $var variable to dump
  54. * @param integer $length maximum length of strings
  55. * @param integer $limit recursion limit
  56. * @param integer $level current recursion level (internal usage only!)
  57. * @return string
  58. */
  59. protected static function _dump( & $var, $length = 128, $limit = 10, $level = 0)
  60. {
  61. if ($var === NULL)
  62. {
  63. return '<small>NULL</small>';
  64. }
  65. elseif (is_bool($var))
  66. {
  67. return '<small>bool</small> '.($var ? 'TRUE' : 'FALSE');
  68. }
  69. elseif (is_float($var))
  70. {
  71. return '<small>float</small> '.$var;
  72. }
  73. elseif (is_resource($var))
  74. {
  75. $type = get_resource_type($var);
  76. if ($type === 'stream' AND ($meta = stream_get_meta_data($var)))
  77. {
  78. if (isset($meta['uri']))
  79. {
  80. $file = $meta['uri'];
  81. if (stream_is_local($file))
  82. {
  83. $file = Debug::path($file);
  84. }
  85. return '<small>resource</small><span>('.$type.')</span> '.htmlspecialchars($file, ENT_NOQUOTES, KO7::$charset);
  86. }
  87. }
  88. else
  89. {
  90. return '<small>resource</small><span>('.$type.')</span>';
  91. }
  92. }
  93. elseif (is_string($var))
  94. {
  95. // Clean invalid multibyte characters. iconv is only invoked
  96. // if there are non ASCII characters in the string, so this
  97. // isn't too much of a hit.
  98. $var = UTF8::clean($var, KO7::$charset);
  99. if (UTF8::strlen($var) > $length)
  100. {
  101. // Encode the truncated string
  102. $str = htmlspecialchars(UTF8::substr($var, 0, $length), ENT_NOQUOTES, KO7::$charset).'&nbsp;&hellip;';
  103. }
  104. else
  105. {
  106. // Encode the string
  107. $str = htmlspecialchars($var, ENT_NOQUOTES, KO7::$charset);
  108. }
  109. return '<small>string</small><span>('.strlen($var).')</span> "'.$str.'"';
  110. }
  111. elseif (is_array($var))
  112. {
  113. $output = [];
  114. // Indentation for this variable
  115. $space = str_repeat($s = ' ', $level);
  116. static $marker;
  117. if ($marker === NULL)
  118. {
  119. // Make a unique marker - force it to be alphanumeric so that it is always treated as a string array key
  120. $marker = uniqid("\x00")."x";
  121. }
  122. if (empty($var))
  123. {
  124. // Do nothing
  125. }
  126. elseif (isset($var[$marker]))
  127. {
  128. $output[] = "(\n$space$s*RECURSION*\n$space)";
  129. }
  130. elseif ($level < $limit)
  131. {
  132. $output[] = "<span>(";
  133. $var[$marker] = TRUE;
  134. foreach ($var as $key => & $val)
  135. {
  136. if ($key === $marker) continue;
  137. if ( ! is_int($key))
  138. {
  139. $key = '"'.htmlspecialchars($key, ENT_NOQUOTES, KO7::$charset).'"';
  140. }
  141. $output[] = "$space$s$key => ".Debug::_dump($val, $length, $limit, $level + 1);
  142. }
  143. unset($var[$marker]);
  144. $output[] = "$space)</span>";
  145. }
  146. else
  147. {
  148. // Depth too great
  149. $output[] = "(\n$space$s...\n$space)";
  150. }
  151. return '<small>array</small><span>('.count($var).')</span> '.implode("\n", $output);
  152. }
  153. elseif (is_object($var))
  154. {
  155. // Copy the object as an array
  156. $array = (array) $var;
  157. $output = [];
  158. // Indentation for this variable
  159. $space = str_repeat($s = ' ', $level);
  160. $hash = spl_object_hash($var);
  161. // Objects that are being dumped
  162. static $objects = [];
  163. if (empty($var))
  164. {
  165. // Do nothing
  166. }
  167. elseif (isset($objects[$hash]))
  168. {
  169. $output[] = "{\n$space$s*RECURSION*\n$space}";
  170. }
  171. elseif ($level < $limit)
  172. {
  173. $output[] = "<code>{";
  174. $objects[$hash] = TRUE;
  175. foreach ($array as $key => & $val)
  176. {
  177. if ($key[0] === "\x00")
  178. {
  179. // Determine if the access is protected or protected
  180. $access = '<small>'.(($key[1] === '*') ? 'protected' : 'private').'</small>';
  181. // Remove the access level from the variable name
  182. $key = substr($key, strrpos($key, "\x00") + 1);
  183. }
  184. else
  185. {
  186. $access = '<small>public</small>';
  187. }
  188. $output[] = "$space$s$access $key => ".Debug::_dump($val, $length, $limit, $level + 1);
  189. }
  190. unset($objects[$hash]);
  191. $output[] = "$space}</code>";
  192. }
  193. else
  194. {
  195. // Depth too great
  196. $output[] = "{\n$space$s...\n$space}";
  197. }
  198. return '<small>object</small> <span>'.get_class($var).'('.count($array).')</span> '.implode("\n", $output);
  199. }
  200. else
  201. {
  202. return '<small>'.gettype($var).'</small> '.htmlspecialchars(print_r($var, TRUE), ENT_NOQUOTES, KO7::$charset);
  203. }
  204. }
  205. /**
  206. * Removes application, system, modpath, or docroot from a filename,
  207. * replacing them with the plain text equivalents. Useful for debugging
  208. * when you want to display a shorter path.
  209. *
  210. * // Displays SYSPATH/classes/KO7.php
  211. * echo Debug::path(KO7::find_file('classes', 'KO7'));
  212. *
  213. * @param string $file path to debug
  214. * @return string
  215. */
  216. public static function path($file)
  217. {
  218. if (strpos($file, APPPATH) === 0)
  219. {
  220. $file = 'APPPATH'.DIRECTORY_SEPARATOR.substr($file, strlen(APPPATH));
  221. }
  222. elseif (strpos($file, SYSPATH) === 0)
  223. {
  224. $file = 'SYSPATH'.DIRECTORY_SEPARATOR.substr($file, strlen(SYSPATH));
  225. }
  226. elseif (strpos($file, MODPATH) === 0)
  227. {
  228. $file = 'MODPATH'.DIRECTORY_SEPARATOR.substr($file, strlen(MODPATH));
  229. }
  230. elseif (strpos($file, DOCROOT) === 0)
  231. {
  232. $file = 'DOCROOT'.DIRECTORY_SEPARATOR.substr($file, strlen(DOCROOT));
  233. }
  234. return $file;
  235. }
  236. /**
  237. * Returns an HTML string, highlighting a specific line of a file, with some
  238. * number of lines padded above and below.
  239. *
  240. * // Highlights the current line of the current file
  241. * echo Debug::source(__FILE__, __LINE__);
  242. *
  243. * @param string $file file to open
  244. * @param integer $line_number line number to highlight
  245. * @param integer $padding number of padding lines
  246. * @return string source of file
  247. * @return FALSE file is unreadable
  248. */
  249. public static function source($file, $line_number, $padding = 5)
  250. {
  251. if ( ! $file OR ! is_readable($file))
  252. {
  253. // Continuing will cause errors
  254. return FALSE;
  255. }
  256. // Open the file and set the line position
  257. $file = fopen($file, 'r');
  258. $line = 0;
  259. // Set the reading range
  260. $range = ['start' => $line_number - $padding, 'end' => $line_number + $padding];
  261. // Set the zero-padding amount for line numbers
  262. $format = '% '.strlen($range['end']).'d';
  263. $source = '';
  264. while (($row = fgets($file)) !== FALSE)
  265. {
  266. // Increment the line number
  267. if (++$line > $range['end'])
  268. break;
  269. if ($line >= $range['start'])
  270. {
  271. // Make the row safe for output
  272. $row = htmlspecialchars($row, ENT_NOQUOTES, KO7::$charset);
  273. // Trim whitespace and sanitize the row
  274. $row = '<span class="number">'.sprintf($format, $line).'</span> '.$row;
  275. if ($line === $line_number)
  276. {
  277. // Apply highlighting to this row
  278. $row = '<span class="line highlight">'.$row.'</span>';
  279. }
  280. else
  281. {
  282. $row = '<span class="line">'.$row.'</span>';
  283. }
  284. // Add to the captured source
  285. $source .= $row;
  286. }
  287. }
  288. // Close the file
  289. fclose($file);
  290. return '<pre class="source"><code>'.$source.'</code></pre>';
  291. }
  292. /**
  293. * Returns an array of HTML strings that represent each step in the backtrace.
  294. *
  295. * // Displays the entire current backtrace
  296. * echo implode('<br/>', Debug::trace());
  297. *
  298. * @param array|null $trace Stack to trace
  299. * @return string
  300. */
  301. public static function trace(array $trace = NULL)
  302. {
  303. if ($trace === NULL)
  304. {
  305. // Start a new trace
  306. $trace = debug_backtrace();
  307. }
  308. // Non-standard function calls
  309. $statements = ['include', 'include_once', 'require', 'require_once'];
  310. $output = [];
  311. foreach ($trace as $step)
  312. {
  313. if ( ! isset($step['function']))
  314. {
  315. // Invalid trace step
  316. continue;
  317. }
  318. if (isset($step['file']) AND isset($step['line']))
  319. {
  320. // Include the source of this step
  321. $source = Debug::source($step['file'], $step['line']);
  322. }
  323. if (isset($step['file']))
  324. {
  325. $file = $step['file'];
  326. if (isset($step['line']))
  327. {
  328. $line = $step['line'];
  329. }
  330. }
  331. // function()
  332. $function = $step['function'];
  333. if (in_array($step['function'], $statements))
  334. {
  335. if (empty($step['args']))
  336. {
  337. // No arguments
  338. $args = [];
  339. }
  340. else
  341. {
  342. // Sanitize the file path
  343. $args = [$step['args'][0]];
  344. }
  345. }
  346. elseif (isset($step['args']))
  347. {
  348. if ( ! function_exists($step['function']) OR strpos($step['function'], '{closure}') !== FALSE)
  349. {
  350. // Introspection on closures or language constructs in a stack trace is impossible
  351. $params = NULL;
  352. }
  353. else
  354. {
  355. if (isset($step['class']))
  356. {
  357. if (method_exists($step['class'], $step['function']))
  358. {
  359. $reflection = new ReflectionMethod($step['class'], $step['function']);
  360. }
  361. else
  362. {
  363. $reflection = new ReflectionMethod($step['class'], '__call');
  364. }
  365. }
  366. else
  367. {
  368. $reflection = new ReflectionFunction($step['function']);
  369. }
  370. // Get the function parameters
  371. $params = $reflection->getParameters();
  372. }
  373. $args = [];
  374. foreach ($step['args'] as $i => $arg)
  375. {
  376. if (isset($params[$i]))
  377. {
  378. // Assign the argument by the parameter name
  379. $args[$params[$i]->name] = $arg;
  380. }
  381. else
  382. {
  383. // Assign the argument by number
  384. $args[$i] = $arg;
  385. }
  386. }
  387. }
  388. if (isset($step['class']))
  389. {
  390. // Class->method() or Class::method()
  391. $function = $step['class'].$step['type'].$step['function'];
  392. }
  393. $output[] = [
  394. 'function' => $function,
  395. 'args' => isset($args) ? $args : NULL,
  396. 'file' => isset($file) ? $file : NULL,
  397. 'line' => isset($line) ? $line : NULL,
  398. 'source' => isset($source) ? $source : NULL,
  399. ];
  400. unset($function, $args, $file, $line, $source);
  401. }
  402. return $output;
  403. }
  404. }