Debug.php 11 KB

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