Source for file Debug.php
Documentation is available at Debug.php
6: * @copyright Copyright (c) 2004, 2010 David Grudl
7: * @license http://nettephp.com/license Nette license
8: * @link http://nettephp.com
16: * Debug static class.
18: * @copyright Copyright (c) 2004, 2010 David Grudl
23: /** @var bool determines whether a server is running in production mode */
24: public static $productionMode;
26: /** @var bool determines whether a server is running in console mode */
27: public static $consoleMode;
32: /** @var bool is Firebug & FirePHP detected? */
33: private static $firebugDetected;
35: /** @var bool is AJAX request detected? */
36: private static $ajaxDetected;
38: /** @var array payload filled by {@link Debug::consoleDump()} */
39: private static $consoleData;
41: /********************* Debug::dump() ****************d*g**/
43: /** @var int how many nested levels of array/object properties display {@link Debug::dump()} */
44: public static $maxDepth =
3;
46: /** @var int how long strings display {@link Debug::dump()} */
47: public static $maxLen =
150;
49: /** @var int display location? {@link Debug::dump()} */
50: public static $showLocation =
FALSE;
52: /********************* errors and exceptions reporing ****************d*g**/
54: /**#@+ server modes {@link Debug::enable()} */
55: const DEVELOPMENT =
FALSE;
56: const PRODUCTION =
TRUE;
60: /** @var bool determines whether to consider all errors as fatal */
61: public static $strictMode =
FALSE;
63: /** @var array of callbacks specifies the functions that are automatically called after fatal error */
64: public static $onFatalError =
array();
67: public static $mailer =
array(__CLASS__
, 'defaultMailer');
69: /** @var int interval for sending email is 2 days */
70: public static $emailSnooze =
172800;
72: /** @var bool {@link Debug::enable()} */
73: private static $enabled =
FALSE;
75: /** @var string name of the file where script errors should be logged */
76: private static $logFile;
79: private static $logHandle;
81: /** @var bool send e-mail notifications of errors? */
82: private static $sendEmails;
84: /** @var string e-mail headers & body */
85: private static $emailHeaders =
array(
87: 'From' =>
'noreply@%host%',
88: 'X-Mailer' =>
'Nette Framework',
89: 'Subject' =>
'PHP: An error occurred on the server %host%',
90: 'Body' =>
'[%date%] %message%',
94: private static $colophons =
array(array(__CLASS__
, 'getDefaultColophons'));
96: /********************* profiler ****************d*g**/
98: /** @var bool {@link Debug::enableProfiler()} */
99: private static $enabledProfiler =
FALSE;
101: /** @var array free counters for your usage */
102: public static $counters =
array();
104: /********************* Firebug extension ****************d*g**/
106: /**#@+ FirePHP log priority */
108: const INFO =
'INFO';
109: const WARN =
'WARN';
110: const ERROR =
'ERROR';
111: const TRACE =
'TRACE';
112: const EXCEPTION =
'EXCEPTION';
113: const GROUP_START =
'GROUP_START';
114: const GROUP_END =
'GROUP_END';
120: * Static class - cannot be instantiated.
124: throw new LogicException("Cannot instantiate static class " .
get_class($this));
130: * Static class constructor.
133: public static function _init()
135: self::$time =
microtime(TRUE);
136: self::$consoleMode =
PHP_SAPI ===
'cli';
137: self::$productionMode =
self::DETECT;
138: self::$firebugDetected =
isset($_SERVER['HTTP_USER_AGENT']) &&
strpos($_SERVER['HTTP_USER_AGENT'], 'FirePHP/');
139: self::$ajaxDetected =
isset($_SERVER['HTTP_X_REQUESTED_WITH']) &&
$_SERVER['HTTP_X_REQUESTED_WITH'] ===
'XMLHttpRequest';
140: register_shutdown_function(array(__CLASS__
, '_shutdownHandler'));
146: * Shutdown handler to execute of the planned activities.
150: public static function _shutdownHandler()
152: // 1) fatal error handler
153: static $types =
array(
156: E_COMPILE_ERROR =>
1,
160: $error =
error_get_last();
161: if (self::$enabled &&
isset($types[$error['type']])) {
162: if (!headers_sent()) { // for PHP < 5.2.4
163: header('HTTP/1.1 500 Internal Server Error');
166: if (ini_get('html_errors')) {
167: $error['message'] =
html_entity_decode(strip_tags($error['message']), ENT_QUOTES, 'UTF-8');
170: self::processException(new FatalErrorException($error['message'], 0, $error['type'], $error['file'], $error['line'], NULL), TRUE);
174: // other activities require HTML & development mode
175: if (self::$productionMode) {
178: foreach (headers_list() as $header) {
180: if (substr($header, 14, 9) ===
'text/html') {
188: if (self::$enabledProfiler) {
189: if (self::$firebugDetected) {
190: self::fireLog('Nette profiler', self::GROUP_START);
191: foreach (self::$colophons as $callback) {
192: foreach ((array)
call_user_func($callback, 'profiler') as $line) self::fireLog(strip_tags($line));
194: self::fireLog(NULL, self::GROUP_END);
197: if (!self::$ajaxDetected) {
198: $colophons =
self::$colophons;
199: require dirname(__FILE__) .
'/Debug.templates/profiler.phtml';
205: if (self::$consoleData) {
206: $payload =
self::$consoleData;
207: require dirname(__FILE__) .
'/Debug.templates/console.phtml';
213: /********************* useful tools ****************d*g**/
218: * Dumps information about a variable in readable format.
220: * @param mixed variable to dump
221: * @param bool return output instead of printing it? (bypasses $productionMode)
222: * @return mixed variable itself or dump
224: public static function dump($var, $return =
FALSE)
226: if (!$return &&
self::$productionMode) {
230: $output =
"<pre class=\"dump\">" .
self::_dump($var, 0) .
"</pre>\n";
232: if (!$return &&
self::$showLocation) {
233: $trace =
debug_backtrace();
234: $i =
isset($trace[1]['class']) &&
$trace[1]['class'] === __CLASS__ ?
1 :
0;
235: if (isset($trace[$i]['file'], $trace[$i]['line'])) {
240: if (self::$consoleMode) {
241: $output =
htmlspecialchars_decode(strip_tags($output), ENT_NOQUOTES);
256: * Dumps information about a variable in Nette Debug Console.
258: * @param mixed variable to dump
259: * @param string optional title
260: * @return mixed variable itself
264: if (!self::$productionMode) {
266: foreach ((is_array($var) ?
$var :
array('' =>
$var)) as $key =>
$val) {
267: $dump[$key] =
self::dump($val, TRUE);
269: self::$consoleData[] =
array('title' =>
$title, 'dump' =>
$dump);
277: * Internal dump() implementation.
279: * @param mixed variable to dump
280: * @param int current recursion level
283: private static function _dump(&$var, $level)
285: static $tableUtf, $tableBin, $re =
'#[^\x09\x0A\x0D\x20-\x7E\xA0-\x{10FFFF}]#u';
286: if ($tableUtf ===
NULL) {
287: foreach (range("\x00", "\xFF") as $ch) {
288: if (ord($ch) <
32 &&
strpos("\r\n\t", $ch) ===
FALSE) $tableUtf[$ch] =
$tableBin[$ch] =
'\\x' .
str_pad(dechex(ord($ch)), 2, '0', STR_PAD_LEFT);
289: elseif (ord($ch) <
127) $tableUtf[$ch] =
$tableBin[$ch] =
$ch;
290: else { $tableUtf[$ch] =
$ch; $tableBin[$ch] =
'\\x' .
dechex(ord($ch)); }
292: $tableUtf['\\x'] =
$tableBin['\\x'] =
'\\\\x';
296: return "<span>bool</span>(" .
($var ?
'TRUE' :
'FALSE') .
")\n";
298: } elseif ($var ===
NULL) {
299: return "<span>NULL</span>\n";
302: return "<span>int</span>($var)\n";
305: return "<span>float</span>($var)\n";
308: if (self::$maxLen &&
strlen($var) >
self::$maxLen) {
309: $s =
htmlSpecialChars(substr($var, 0, self::$maxLen), ENT_NOQUOTES) .
' ... ';
311: $s =
htmlSpecialChars($var, ENT_NOQUOTES);
314: return "<span>string</span>(" .
strlen($var) .
") \"$s\"\n";
317: $s =
"<span>array</span>(" .
count($var) .
") ";
321: if ($marker ===
NULL) $marker =
uniqid("\x00", TRUE);
324: } elseif (isset($var[$marker])) {
325: $s .=
"{\n$space$space1*RECURSION*\n$space}";
327: } elseif ($level <
self::$maxDepth ||
!self::$maxDepth) {
330: foreach ($var as $k =>
&$v) {
331: if ($k ===
$marker) continue;
333: $s .=
"$space$space1$k => " .
self::_dump($v, $level +
1);
335: unset($var[$marker]);
336: $s .=
"$space}</code>";
339: $s .=
"{\n$space$space1...\n$space}";
344: $arr = (array)
$var;
348: static $list =
array();
353: $s .=
"{\n$space$space1*RECURSION*\n$space}";
355: } elseif ($level <
self::$maxDepth ||
!self::$maxDepth) {
358: foreach ($arr as $k =>
&$v) {
360: if ($k[0] ===
"\x00") {
361: $m =
$k[1] ===
'*' ?
' <span>protected</span>' :
' <span>private</span>';
365: $s .=
"$space$space1\"$k\"$m => " .
self::_dump($v, $level +
1);
368: $s .=
"$space}</code>";
371: $s .=
"{\n$space$space1...\n$space}";
379: return "<span>unknown type</span>\n";
386: * Starts/stops stopwatch.
387: * @param string name
388: * @return elapsed seconds
390: public static function timer($name =
NULL)
392: static $time =
array();
393: $now =
microtime(TRUE);
394: $delta =
isset($time[$name]) ?
$now -
$time[$name] :
0;
395: $time[$name] =
$now;
401: /********************* errors and exceptions reporing ****************d*g**/
406: * Enables displaying or logging errors and exceptions.
407: * @param mixed production, development mode, autodetection or IP address(es).
408: * @param string error log file (FALSE disables logging in production mode)
409: * @param array|string administrator email or email headers; enables email sending in production mode
412: public static function enable($mode =
NULL, $logFile =
NULL, $email =
NULL)
416: // production/development mode detection
418: self::$productionMode =
$mode;
420: } elseif (is_string($mode)) { // IP adresses
425: self::$productionMode =
!isset($_SERVER['REMOTE_ADDR']) ||
!in_array($_SERVER['REMOTE_ADDR'], $mode, TRUE);
428: if (self::$productionMode ===
self::DETECT) {
430: self::$productionMode =
Environment::isProduction();
432: } elseif (isset($_SERVER['SERVER_ADDR']) ||
isset($_SERVER['LOCAL_ADDR'])) { // IP address based detection
433: $addr =
isset($_SERVER['SERVER_ADDR']) ?
$_SERVER['SERVER_ADDR'] :
$_SERVER['LOCAL_ADDR'];
435: self::$productionMode =
$addr !==
'::1' &&
(count($oct) !==
4 ||
($oct[0] !==
'10' &&
$oct[0] !==
'127' &&
($oct[0] !==
'172' ||
$oct[1] <
16 ||
$oct[1] >
31)
436: &&
($oct[0] !==
'169' ||
$oct[1] !==
'254') &&
($oct[0] !==
'192' ||
$oct[1] !==
'168')));
439: self::$productionMode =
!self::$consoleMode;
443: // logging configuration
444: if (self::$productionMode &&
$logFile !==
FALSE) {
445: self::$logFile =
'log/php_error.log';
447: if (class_exists('Environment')) {
449: self::$logFile =
Environment::expand($logFile);
452: self::$logFile =
Environment::expand('%logDir%/php_error.log');
458: self::$logFile =
$logFile;
461: ini_set('error_log', self::$logFile);
464: // php configuration
465: if (function_exists('ini_set')) {
466: ini_set('display_errors', !self::$productionMode); // or 'stderr'
467: ini_set('html_errors', !self::$logFile &&
!self::$consoleMode);
468: ini_set('log_errors', (bool)
self::$logFile);
470: } elseif (ini_get('log_errors') != (bool)
self::$logFile ||
// intentionally ==
471: (ini_get('display_errors') !=
!self::$productionMode &&
ini_get('display_errors') !==
(self::$productionMode ?
'stderr' :
'stdout'))) {
472: throw new LogicException('Function ini_set() must be enabled.');
475: self::$sendEmails =
self::$logFile &&
$email;
476: if (self::$sendEmails) {
477: if (is_string($email)) {
478: self::$emailHeaders['To'] =
$email;
480: } elseif (is_array($email)) {
481: self::$emailHeaders =
$email +
self::$emailHeaders;
485: if (!defined('E_DEPRECATED')) {
495: self::$enabled =
TRUE;
501: * Unregister error handler routine.
506: return self::$enabled;
512: * Debug exception handler.
518: public static function _exceptionHandler(Exception $exception)
521: header('HTTP/1.1 500 Internal Server Error');
523: self::processException($exception, TRUE);
530: * Own error handler.
532: * @param int level of the error raised
533: * @param string error message
534: * @param string file that the error was raised in
535: * @param int line number the error was raised at
536: * @param array an array of variables that existed in the scope the error was triggered in
537: * @return bool FALSE to call normal error handler, NULL otherwise
538: * @throws FatalErrorException
541: public static function _errorHandler($severity, $message, $file, $line, $context)
543: if ($severity ===
E_RECOVERABLE_ERROR ||
$severity ===
E_USER_ERROR) {
547: return NULL; // nothing to do
549: } elseif (self::$strictMode) {
553: static $types =
array(
554: E_WARNING =>
'Warning',
555: E_USER_WARNING =>
'Warning',
556: E_NOTICE =>
'Notice',
557: E_USER_NOTICE =>
'Notice',
558: E_STRICT =>
'Strict standards',
559: E_DEPRECATED =>
'Deprecated',
560: E_USER_DEPRECATED =>
'Deprecated',
563: $type =
isset($types[$severity]) ?
$types[$severity] :
'Unknown error';
565: if (self::$logFile) {
566: if (self::$sendEmails) {
567: self::sendEmail("$type:
$message in
$file on line
$line");
569: return FALSE; // call normal error handler
571: } elseif (!self::$productionMode &&
self::$firebugDetected &&
!headers_sent()) {
573: self::fireLog("$type: $message in $file on line $line", self::ERROR);
577: return FALSE; // call normal error handler
583: * Logs or displays exception.
585: * @param bool is writing to standard output buffer allowed?
590: if (!self::$enabled) {
593: } elseif (self::$logFile) {
595: $hash =
md5($exception .
(method_exists($exception, 'getPrevious') ?
$exception->getPrevious() :
(isset($exception->previous) ?
$exception->previous :
'')));
597: foreach (new DirectoryIterator(dirname(self::$logFile)) as $entry) {
598: if (strpos($entry, $hash)) {
603: $file =
dirname(self::$logFile) .
"/exception " .
@date('Y-m-d H-i-s') .
" $hash.html";
604: if (empty($skip) &&
self::$logHandle =
@fopen($file, 'x')) {
605: ob_start(); // double buffer prevents sending HTTP headers in some PHP
607: self::_paintBlueScreen($exception);
612: if (self::$sendEmails) {
613: self::sendEmail((string)
$exception);
615: } catch (Exception $e) {
617: header('HTTP/1.1 500 Internal Server Error');
619: echo 'Nette\Debug fatal error: ', get_class($e), ': ', ($e->getCode() ?
'#' .
$e->getCode() .
' ' :
'') .
$e->getMessage(), "\n";
623: } elseif (self::$productionMode) {
626: } elseif (self::$consoleMode) { // dump to console
627: if ($outputAllowed) {
628: echo "$exception\n";
629: foreach (self::$colophons as $callback) {
630: foreach ((array)
call_user_func($callback, 'bluescreen') as $line) echo strip_tags($line) .
"\n";
634: } elseif (self::$firebugDetected &&
self::$ajaxDetected &&
!headers_sent()) { // AJAX mode
635: self::fireLog($exception, self::EXCEPTION);
637: } elseif ($outputAllowed) { // dump to browser
643: self::_paintBlueScreen($exception);
645: } elseif (self::$firebugDetected &&
!headers_sent()) {
646: self::fireLog($exception, self::EXCEPTION);
649: foreach (self::$onFatalError as $handler) {
650: call_user_func($handler, $exception);
657: * Handles exception throwed in __toString().
663: if (self::$enabled) {
664: self::_exceptionHandler($exception);
673: * Paint blue screen.
678: public static function _paintBlueScreen(Exception $exception)
680: $internals =
array();
681: foreach (array('Object', 'ObjectMixin') as $class) {
683: $rc =
new ReflectionClass($class);
684: $internals[$rc->getFileName()] =
TRUE;
692: $colophons =
self::$colophons;
693: require dirname(__FILE__) .
'/Debug.templates/bluescreen.phtml';
699: * Redirects output to file.
704: public static function _writeFile($buffer)
712: * Sends e-mail notification.
716: private static function sendEmail($message)
718: $monitorFile =
self::$logFile .
'.monitor';
719: if (@filemtime($monitorFile) +
self::$emailSnooze <
time()
732: private static function defaultMailer($message)
734: $host =
isset($_SERVER['HTTP_HOST']) ?
$_SERVER['HTTP_HOST'] :
735: (isset($_SERVER['SERVER_NAME']) ?
$_SERVER['SERVER_NAME'] :
'');
738: array('%host%', '%date%', '%message%'),
739: array($host, @date('Y-m-d H:i:s', self::$time), $message), // intentionally @
743: $subject =
$headers['Subject'];
744: $to =
$headers['To'];
745: $body =
$headers['Body'];
746: unset($headers['Subject'], $headers['To'], $headers['Body']);
748: foreach ($headers as $key =>
$value) {
749: $header .=
"$key: $value\r\n";
752: // we need to change \r\n to \n because Unix mailer changes it back to \r\n
753: $body =
str_replace("\r\n", "\n", $body);
756: mail($to, $subject, $body, $header);
761: /********************* profiler ****************d*g**/
771: self::$enabledProfiler =
TRUE;
777: * Disables profiler.
782: self::$enabledProfiler =
FALSE;
787: /********************* colophons ****************d*g**/
792: * Add custom descriptions.
800: throw new InvalidArgumentException("Colophon handler '$textual' is not " .
($able ?
'callable.' :
'valid PHP callback.'));
804: self::$colophons[] =
$callback;
811: * Returns default colophons.
812: * @param string profiler | bluescreen
815: private static function getDefaultColophons($sender)
817: if ($sender ===
'profiler') {
820: foreach ((array)
self::$counters as $name =>
$value) {
821: if (is_array($value)) $value =
implode(', ', $value);
822: $arr[] =
htmlSpecialChars($name) .
' = <strong>' .
htmlSpecialChars($value) .
'</strong>';
828: $exclude =
array('stdClass', 'Exception', 'ErrorException', 'Traversable', 'IteratorAggregate', 'Iterator', 'ArrayAccess', 'Serializable', 'Closure');
830: $ref =
new ReflectionExtension($ext);
836: $func = (array)
@$func['user'];
839: foreach (array('classes', 'intf', 'func', 'consts') as $item) {
840: $s .=
'<span ' .
($
$item ?
'title="' .
implode(", ", $
$item) .
'"' :
'') .
'>' .
count($
$item) .
' ' .
$item .
'</span>, ';
845: if ($sender ===
'bluescreen') {
846: $arr[] =
'Report generated at ' .
@date('Y/m/d H:i:s', self::$time); // intentionally @
847: if (isset($_SERVER['HTTP_HOST'], $_SERVER['REQUEST_URI'])) {
848: $url =
(isset($_SERVER['HTTPS']) &&
strcasecmp($_SERVER['HTTPS'], 'off') ?
'https://' :
'http://') .
htmlSpecialChars($_SERVER['HTTP_HOST'] .
$_SERVER['REQUEST_URI']);
849: $arr[] =
'<a href="' .
$url .
'">' .
$url .
'</a>';
851: $arr[] =
'PHP ' .
htmlSpecialChars(PHP_VERSION);
852: if (isset($_SERVER['SERVER_SOFTWARE'])) $arr[] =
htmlSpecialChars($_SERVER['SERVER_SOFTWARE']);
853: if (class_exists('Framework')) $arr[] =
htmlSpecialChars('Nette Framework ' .
Framework::VERSION) .
' <i>(revision ' .
htmlSpecialChars(Framework::REVISION) .
')</i>';
860: /********************* Firebug extension ****************d*g**/
865: * Sends variable dump to Firebug tab request/server.
866: * @param mixed variable to dump
867: * @param string unique key
868: * @return mixed variable itself
872: self::fireSend('Dump/0.1', array((string)
$key =>
$var));
879: * Sends message to Firebug console.
880: * @param mixed message to log
881: * @param string priority of message (LOG, INFO, WARN, ERROR, GROUP_START, GROUP_END)
882: * @param string optional label
883: * @return bool was successful?
885: public static function fireLog($message, $priority =
self::LOG, $label =
NULL)
887: if ($message instanceof
Exception) {
888: if ($priority !==
self::EXCEPTION &&
$priority !==
self::TRACE) {
889: $priority =
self::TRACE;
893: 'Message' =>
$message->getMessage(),
894: 'File' =>
$message->getFile(),
895: 'Line' =>
$message->getLine(),
896: 'Trace' =>
$message->getTrace(),
900: foreach ($message['Trace'] as & $row) {
901: if (empty($row['file'])) $row['file'] =
'?';
902: if (empty($row['line'])) $row['line'] =
'?';
904: } elseif ($priority ===
self::GROUP_START) {
908: return self::fireSend('FirebugConsole/0.1', self::replaceObjects(array(array('Type' =>
$priority, 'Label' =>
$label), $message)));
914: * Performs Firebug output.
915: * @see http://www.firephp.org
916: * @param string structure
917: * @param array payload
918: * @return bool was successful?
920: private static function fireSend($struct, $payload)
922: if (self::$productionMode) return NULL;
926: header('X-Wf-Protocol-nette: http://meta.wildfirehq.org/Protocol/JsonStream/0.2');
927: header('X-Wf-nette-Plugin-1: http://meta.firephp.org/Wildfire/Plugin/FirePHP/Library-FirePHPCore/0.2.0');
930: $index =
isset($structures[$struct]) ?
$structures[$struct] :
($structures[$struct] =
count($structures) +
1);
931: header("X-Wf-nette-Structure-$index: http://meta.firephp.org/Wildfire/Structure/FirePHP/$struct");
951: static private function replaceObjects($val)
957: return @iconv('UTF-16', 'UTF-8//IGNORE', iconv('UTF-8', 'UTF-16//IGNORE', $val)); // intentionally @
960: foreach ($val as $k =>
$v) {
962: $k =
@iconv('UTF-16', 'UTF-8//IGNORE', iconv('UTF-8', 'UTF-16//IGNORE', $k)); // intentionally @
963: $val[$k] =
self::replaceObjects($v);