From 28c924f0d34f9a8b476c8295526d77b8574ce5ea Mon Sep 17 00:00:00 2001 From: Brad Kent Date: Thu, 17 Jul 2014 21:53:35 -0500 Subject: [PATCH] fixes / enhancements Fix: #1: outputAs not being passed to onOutput callback on fatal errors Fix: #2: firePHP's client will abort output whene non-utf-8 encountered Fix: #3: objects not cloned / output() method is destructive Enhancement: Pass file & line number to firephp for error and warn methods Enhancement: table method: "undefined" value no longer appears as null, but as empty table cell with class t_undefined Enhancement: error summary at the top if there was an error Enhancement/Fix: boolean & null values sent to FirePHP are now sent as boolean & null, rather than string representations --- Debug.php | 392 ++++++++++++++++++++++++++++++++++++++---------------- README.md | 4 +- 2 files changed, 281 insertions(+), 115 deletions(-) diff --git a/Debug.php b/Debug.php index c6c9c49f..3b401c45 100644 --- a/Debug.php +++ b/Debug.php @@ -1,10 +1,18 @@ 'Parsing Error', // handled via shutdown function E_NOTICE => 'Notice', E_CORE_ERROR => 'Core Error', // handled via shutdown function - E_CORE_WARNING => 'Core Warning', // not handled + E_CORE_WARNING => 'Core Warning', // handled? E_COMPILE_ERROR => 'Compile Error', // handled via shutdown function - E_COMPILE_WARNING => 'Compile Warning', // not handled + E_COMPILE_WARNING => 'Compile Warning', // handled? E_USER_ERROR => 'User Error', E_USER_WARNING => 'User Warning', E_USER_NOTICE => 'User Notice', - E_ALL => 'E_ALL', // only listed here for completeness - E_STRICT => 'Runtime Notice (E_STRICT)', // PHP 5.0.0 - E_RECOVERABLE_ERROR => 'Fatal Error', // PHP 5.2.0 - E_DEPRECATED => 'Deprecated', // PHP 5.3.0 - E_USER_DEPRECATED => 'User Deprecated', // PHP 5.3.0 + E_ALL => 'E_ALL', // listed here for completeness + E_STRICT => 'Runtime Notice (E_STRICT)', + E_RECOVERABLE_ERROR => 'Fatal Error', + E_DEPRECATED => 'Deprecated', + E_USER_DEPRECATED => 'User Deprecated', + ); + + var $errTypesGrouped = array( + 'deprecated' => array( E_DEPRECATED, E_USER_DEPRECATED ), + 'error' => array( E_USER_ERROR, E_RECOVERABLE_ERROR ), + 'notice' => array( E_NOTICE, E_USER_NOTICE ), + 'strict' => array( E_STRICT ), + 'warning' => array( E_WARNING, E_CORE_WARNING, E_COMPILE_WARNING, E_USER_WARNING ), + 'fatal' => array( E_ERROR, E_PARSE, E_COMPILE_ERROR, E_CORE_ERROR ), ); /** - * constructor + * constructor (php 4 compatable) * * @param array $cfg config * @@ -62,21 +79,19 @@ function Debug($cfg=array()) 'emailTo' => !empty($_SERVER['SERVER_ADMIN']) ? $_SERVER['SERVER_ADMIN'] : null, - 'onOutput' => null, // set to something callable + 'onOutput' => null, // set to something callable 'errorHandler' => array( 'onError' => null, // set to something callable, will receive a single boolean indicating whether error was fatal 'emailMin' => 15, 'emailMask' => E_ERROR | E_PARSE | E_COMPILE_ERROR | E_WARNING | E_USER_ERROR | E_USER_NOTICE, 'emailTraceMask' => E_WARNING | E_USER_ERROR | E_USER_NOTICE, - 'fatalMask' => E_ERROR | E_PARSE | E_COMPILE_ERROR | E_CORE_ERROR, + 'fatalMask' => array_reduce($this->errTypesGrouped['fatal'], create_function('$a, $b', 'return $a | $b;')), 'emailThrottleFile' => dirname(__FILE__).'/error_emails.txt', ), ), 'data' => array( - 'counts' => array(), + 'counts' => array(), // count method 'errorHandler' => array( - //'collect' => false, // whether collect was on @ time of error - //'email' => false, 'errorCaller' => array(), 'errors' => array(), 'lastError' => array(), @@ -86,8 +101,8 @@ function Debug($cfg=array()) 'groupDepth' => 0, 'groupDepthFile'=> 0, 'log' => array(), - 'recursiveArray' => false, - 'timers' => array( + 'recursion' => false, + 'timers' => array( // timer method 'labels' => array( 'debugInit' => microtime(), ), @@ -329,6 +344,58 @@ function output() call_user_func($this->cfg['onOutput'], $outputAs); } $outputAs = $this->get('outputAs'); + + /** + * create an error summary if there were errors + */ + if ( !empty($this->data['errorHandler']['errors']) ) { + $counts = array(); + foreach ( $this->data['errorHandler']['errors'] as $error ) { + if ( $error['suppressed'] ) + continue; + foreach ( $this->errTypesGrouped as $k => $errTypes ) { + if ( !in_array($error['type'], $errTypes) ) + continue; + $counts[$k] = isset($counts[$k]) + ? $counts[$k] + 1 + : 1; + break; + } + } + if ( $counts ) { + $tot = array_sum($counts); + ksort($counts); + if ( count($counts) == 1 ) { + // all same type of error + $type = key($counts); + if ( $tot == 1 ) { + $alert = 'There was 1 error'; + if ( $type == 'fatal' ) { + $alert = null; // don't bother with this alert.. + // fatal are still prominently displayed + } elseif ( $type != 'error' ) { + $alert .= ' ('.$type.')'; + } + } else { + $alert = 'There were '.$tot.' errors'; + if ( $type != 'error' ) + $alert .= ' of type '.$type; + } + if ( $alert ) + $alert = '

'.$alert.'

'."\n"; + } else { + $alert = '

There were '.$tot.' errors:

'."\n"; + $alert .= ''; + } + if ( $alert ) + $this->alert = $alert; + } + } + $this->data['groupDepth'] = 0; if ( $outputAs == 'html' ) { $return = $this->_outputHtml(); @@ -702,7 +769,6 @@ function emailErr($errType, $errmsg, $file, $line, $vars=array()) $email_body .= "\n".'backtrace: '.$str; } mail($this->cfg['emailTo'], $subject, $email_body); - //$this->log('
email_body:'."\n".$email_body.'
'); } return; } @@ -924,6 +990,7 @@ function getDisplayTable($array,$caption=null) $str = '
'.$str.'
'; } else { $keys = $this->arrayColKeys($array); + $undefined = "\x00".'undefined'."\x00"; $str = ''."\n"; // style="border:solid 1px;" $values = array(); foreach ( $keys as $key ) { @@ -943,12 +1010,14 @@ function getDisplayTable($array,$caption=null) if ( is_array($row) ) { $value = array_key_exists($key, $row) ? $row[$key] - : null; + : $undefined; } elseif ( $key === '' ) { $value = $row; } if ( is_array($value) ) { $value = call_user_func(array($this,__FUNCTION__), $value); + } elseif ( $value === $undefined ) { + $value = ''; } else { $value = $this->getDisplayValue($value); } @@ -958,7 +1027,7 @@ function getDisplayTable($array,$caption=null) foreach ( $values as $v ) { // remove the span wrapper.. add span's class to TD $class = null; - if ( preg_match('#^(.+)$#s', $v, $matches) ) { + if ( preg_match('#^(.*)$#s', $v, $matches) ) { $class = $matches[1]; $v = $matches[2]; } @@ -976,16 +1045,23 @@ function getDisplayTable($array,$caption=null) } /** - * @param mixed $v value - * @param bool $htmlout true - * @param array $hist {@internal - used to check for recursion} + * @param mixed $v value + * @param array $opts options + * @param array $hist {@internal - used to check for recursion} * * @return string */ - function getDisplayValue($v, $htmlout=true, $hist=array()) + function getDisplayValue($v, $opts=array(), $hist=array()) { $type = null; $typeMore = null; + if ( empty($hist) ) { + $opts = array_merge(array( + 'html' => true, // use html markup + 'flatten' => false, // flatten array & obj structures (only applies when !html) + 'boolNullToString' => true, + ), $opts); + } if ( is_string($v) ) { $type = 'string'; if ( is_numeric($v) ) { @@ -993,11 +1069,10 @@ function getDisplayValue($v, $htmlout=true, $hist=array()) } elseif ( $this->isBinary($v) ) { // all or partially binary data $typeMore = 'binary'; - $v = $this->getDisplayBinary($v, $htmlout); - } elseif ( preg_match('/^Resource id #\d+:/', $v) ) { - $type = null; // already designated - } elseif ( !preg_match('#\n|getDisplayBinary($v, $opts['html']); + } elseif ( preg_match('/^(Resource id #\d+:.*?)<\/span>/', $v, $matches) ) { + $type = 'resource'; + $v = $matches[1]; } } elseif ( is_int($v) ) { $type = 'int'; @@ -1005,11 +1080,14 @@ function getDisplayValue($v, $htmlout=true, $hist=array()) $type = 'float'; } elseif ( is_bool($v) ) { $type = 'bool'; - $v = $v ? 'true' : 'false'; - $typeMore = $v; + $vStr = $v ? 'true' : 'false'; + if ( $opts['boolNullToString'] ) + $v = $vStr; + $typeMore = $vStr; } elseif ( is_null($v) ) { $type = 'null'; - $v = 'null'; + if ( $opts['boolNullToString'] ) + $v = 'null'; } elseif ( is_resource($v) ) { $type = 'resource'; $v = print_r($v, true).': '.get_resource_type($v); @@ -1017,18 +1095,24 @@ function getDisplayValue($v, $htmlout=true, $hist=array()) $type = 'array'; if ( empty($hist) ) { // check if we need to be on the look out for self-reference loop - $this->data['recursiveArray'] = $this->isRecursiveArray($v); + $this->data['recursion'] = $this->isRecursive($v); } - $hist[] = &$v; + $hist[] = 'array'; foreach ( $v as $k => $v2 ) { - if ( $this->data['recursiveArray'] && $this->isRecursiveArray($v2, $k) ) { + if ( $this->data['recursion'] && $this->isRecursive($v2, $k) ) { // this is where the recursion is - $v[$k] = '*RECURSION*'; + $v2 = is_object($v2) + ? ''.get_class($v2).' object ' + : 'Array '; + $v2 .= '*RECURSION*'; + if ( !$opts['html'] ) + $v2 = strip_tags($v2); } else { - $v[$k] = $this->getDisplayValue($v2, $htmlout, $hist); + $v2 = $this->getDisplayValue($v2, $opts, $hist); } + $v[$k] = $v2; } - if ( !$htmlout ) { + if ( $opts['flatten'] ) { $v = trim(print_r($v, true)); if ( count($hist) > 1 ) { $v = str_replace("\n", "\n ", $v); @@ -1036,29 +1120,45 @@ function getDisplayValue($v, $htmlout=true, $hist=array()) } } elseif ( is_object($v) ) { $type = 'object'; + if ( empty($hist) ) { + // check if we need to be on the look out for self-reference loop + $this->data['recursion'] = $this->isRecursive($v); + } if ( in_array($v, $hist, true) ) { - $v = ''.get_class($v).' object *RECURSION*'; + $v = ''.get_class($v).' object ' + .'*RECURSION*'; + if ( !$opts['html'] ) { + $v = strip_tags($v); + } } else { $hist[] = &$v; - $className = get_class($v).' object'; - $vars = $this->getDisplayValue(get_object_vars($v), $htmlout, $hist); - $methods = $this->getDisplayValue(get_class_methods($v), $htmlout, $hist); - $v = $className."\n" - .' methods: '.$methods."\n" - .' vars: '.$vars; - if ( !$htmlout && count($hist) > 1 ) { - $v = str_replace("\n", "\n ", $v); + $v = array( + 'className' => get_class($v).' object', + 'vars' => $this->getDisplayValue(get_object_vars($v), $opts, $hist), + 'methods' => $this->getDisplayValue(get_class_methods($v), $opts, $hist), + ); + if ( $opts['html'] || $opts['flatten'] ) { + $v = $v['className']."\n" + .' methods: '.$v['methods']."\n" + .' vars: '.$v['vars']; + if ( $opts['flatten'] && count($hist) > 1 ) { + $v = str_replace("\n", "\n ", $v); + } } } } - if ( $htmlout ) { + if ( $opts['html'] ) { if ( in_array($type, array('array','object')) ) { if ( $type == 'array' ) { $str = 'Array
'."\n" .'('."\n" .''."\n"; foreach ( $v as $k => $v2 ) { - $str .= "\t".'['.$k.'] => '.$v2.''."\n"; + $str .= "\t".'' + .'['.$k.'] ' + .'=> ' + .$v2 + .''."\n"; } $str .= '' .')'; @@ -1066,8 +1166,6 @@ function getDisplayValue($v, $htmlout=true, $hist=array()) } elseif ( $type == 'object' ) { $v = preg_replace('#^([^\n]+)\n(.+)$#s', '\1'."".'\2', $v); $v = preg_replace('#\svars: #', '
vars: ', $v); - // needs more indent - //$v = str_replace("\n", "\n ", $v); } $v = ''.$v.''; } elseif ( $type ) { @@ -1112,19 +1210,7 @@ function shutdownFunction() $error = error_get_last(); if ( $error['type'] & $this->cfg['errorHandler']['fatalMask'] ) { $this->errorHandler($error['type'], $error['message'], $error['file'], $error['line']); - if ( $this->output ) { - if ( is_callable($this->cfg['onOutput']) ) { - /** - * calling onOutput here because the lastError output will send headers - * and any onOutput callback may do a headers_sent() check - */ - call_user_func($this->cfg['onOutput']); - $this->setCfg('onOutput', null); - } - if ( $this->get('outputAs') == 'html' ) - echo '
'; print_r($this->get('lastError')); echo '
'; - echo $this->output(); - } + echo $this->output(); } } /** @@ -1210,8 +1296,9 @@ function _appendLog($method, $args) foreach ( $args as $i => $v ) { // if want to identify the resource type, needs done before resource is closed if ( is_resource($v) ) + // @todo should also check do this inside arrays, etc.. $args[$i] = $this->getDisplayValue($v); - elseif ( is_array($v) ) + elseif ( is_array($v) || is_object($v) ) $args[$i] = $this->removeReferences($v); } array_unshift($args, $method); @@ -1274,7 +1361,7 @@ function _appendLogFile($args) $args[0] = strip_tags($args[0]); foreach ( $args as $k => $v ) { if ( $k > 0 || !is_string($v) ) { - $v = $this->getDisplayValue($v, false); + $v = $this->getDisplayValue($v, array('html'=>false, 'flatten'=>true)); $v = preg_replace('#(.*?)#', '\\1', $v); $args[$k] = $v; } @@ -1328,14 +1415,13 @@ function _getCss() $return = << 2, //'maxDepth' => 2, )); + if ( !empty($this->alert) ) { + $alert = str_replace('
', "\n", $this->alert); + array_unshift($this->data['log'], array('error', $alert)); + } foreach ( $this->data['log'] as $i => $args ) { $method = array_shift($args); + $opts = array(); + foreach ( $args as $k => $arg ) { + $args[$k] = $this->getDisplayValue($arg, array('html'=>false,'boolNullToString'=>false)); + } if ( in_array($method, array('group','groupCollapsed')) ) { $this->data['groupDepth']++; $method = 'group'; @@ -1482,7 +1606,7 @@ function _outputFirephp() $more = array(); while ( count($args) > 1 ) { $v = array_splice($args, 1, 1); - $more[] = $this->getDisplayValue(reset($v), false); + $more[] = reset($v); } $args[0] .= ' - '.implode(', ', $more); } @@ -1505,8 +1629,6 @@ function _outputFirephp() $i++; } } - // - $args[1] = $opts; } elseif ( $method == 'groupEnd' ) { $this->data['groupDepth']--; } elseif ( $method == 'table' && is_array($args[0]) ) { @@ -1531,10 +1653,13 @@ function _outputFirephp() $method = 'log'; } else { if ( in_array($method, array('error','warn')) ) { - // discard errorCaller array, if present $end = end($args); if ( is_array($end) && isset($end['__errorCaller__']) ) { - $a = array_pop($args); + array_pop($args); + $opts = array( + 'File' => $end['file'], + 'Line' => $end['line'], + ); } } if ( count($args) > 1 ) { @@ -1549,6 +1674,12 @@ function _outputFirephp() } if ( !in_array($method, $firephpMethods) ) $method = 'log'; + if ( $opts ) { + // opts array needs to be 2nd arg for group method, 3rd arg for all others + if ( $method !== 'group' && count($args) == 1 ) + $args[] = null; + $args[] = $opts; + } call_user_func_array(array($firephp,$method), $args); } while ( $this->data['groupDepth'] > 0 ) { @@ -1569,8 +1700,14 @@ function _outputHtml() .$this->_getCss()."\n" .''."\n"; } - $str .= '

Debug Log:

'."\n" - .'
'."\n"; + $lastError = $this->get('lastError'); + if ( $lastError && $lastError['type'] & $this->cfg['errorHandler']['fatalMask'] ) { + array_unshift($this->data['log'], array('error error-fatal',$lastError)); + } + $str .= '

Debug Log:

'."\n"; + if ( !empty($this->alert) ) + $str .= '
'.$this->alert.'
'; + $str .= '
'."\n"; foreach ( $this->data['log'] as $k_log => $args ) { $method = array_shift($args); if ( in_array($method, array('group','groupCollapsed')) ) { @@ -1637,6 +1774,7 @@ function _outputHtml() $args[$k] = $this->visualWhiteSpace($v); } } + /* $wrapPre = false; foreach ( $args as $i => $arg ) { if ( strpos($arg, '$#s', '', $arg); } } + */ $args = implode($glue, $args); + /* if ( $wrapPre ) { $args = '
'.$args.'
'; } + */ $str .= '
buildAttribString($attribs).'>'.$args.'
'; } $str .= "\n"; @@ -1820,18 +1961,19 @@ function isBinary($str) * @internal * @link http://stackoverflow.com/questions/9105816/is-there-a-way-to-detect-circular-arrays-in-pure-php */ - function isRecursiveArray($array, $k=null) + function isRecursive($array, $k=null) { $recursive = false; - if ( strpos(print_r($array, true), "Array\n *RECURSION*\n") !== false ) { + //"Array *RECURSION" or "Object *RECURSION*" + if ( strpos(print_r($array, true), "\n *RECURSION*\n") !== false ) + $recursive = true; + if ( $recursive && is_array($array) && $k !== null ) { // array contains recursion or a string containing "Array *RECURSION*" - $unique = new stdclass(); - $recursive = $this->isRecursiveArrayIteration($array, $unique); - if ( $recursive && $k !== null ) { + $recursive = $this->isRecursiveIteration($array); + if ( $recursive ) { // && $k !== null // test if this is the value that's the reference $recursive = $k === $recursive[0]; } - $recursive = !empty($recursive); } return $recursive; } @@ -1846,11 +1988,13 @@ function isRecursiveArray($array, $k=null) * * @return mixed false, or path to reference * @internal - * @used-by isRecursiveArray() + * @used-by isRecursive() */ - function isRecursiveArrayIteration(&$array, $unique, $path=array()) + function isRecursiveIteration(&$array, $unique=null, $path=array()) { - if ( $unique === end($array) ) { + if ( $unique === null ) { + $unique = new stdclass(); + } elseif ( $unique === end($array) ) { return $path; } $array[] = $unique; @@ -1860,19 +2004,19 @@ function isRecursiveArrayIteration(&$array, $unique, $path=array()) $path_new = $path; $path_new[] = $k; if ( is_array($v) ) { - $path_new = $this->isRecursiveArrayIteration($v, $unique, $path_new); + $path_new = $this->isRecursiveIteration($v, $unique, $path_new); if ( $path_new ) { - if ( $unique === end($array) ) { + if ( end($array) === $unique ) { unset($array[key($array)]); } return $path_new; } } } - if ( $unique === end($array) ) { + if ( end($array) === $unique ) { unset($array[key($array)]); } - return false; + return array(); } /** @@ -1914,36 +2058,58 @@ function isUtf8($str, &$ctrl=false) * Remove any reference to an "external" variable * self-referencing array loops are left in place * - * @param mixed $v value to dereference + * @param mixed $a array or object to dereference + * @param array $hist (@internal) * @param array $path {@internal} * * @return mixed * @link http://php.net/manual/en/language.references.php */ - function removeReferences($v,$path=array()) + function removeReferences($a,$hist=array(),$path=array()) { - if ( is_array($v) ) { - if ( !$path ) { - // check if we need to be on the look out for self-reference loop - $this->data['recursiveArray'] = $this->isRecursiveArray($v); - } - if ( $path - && $this->data['recursiveArray'] - && $this->isRecursiveArray($v, end($path)) + if ( empty($hist) ) { + // check if we need to be on the look out for self-reference loop + $this->data['recursion'] = $this->isRecursive($a); + } + $vars = array(); + if ( is_array($a) ) { + $type = 'array'; + if ( !$path + || !$this->data['recursion'] + || !$this->isRecursive($a, end($path)) ) { - // self referencing array loop.. leave as is + $hist[] = &$a; + $vars = $a; + } + } elseif ( is_object($a) ) { + $type = 'object'; + $a_clone = null; + if ( !in_array($a, $hist, true) ) { + $hist[] = &$a; + $vars = get_object_vars($a); + } + if ( version_compare(PHP_VERSION, '5.0', '>=') ) { + $a_clone = clone $a; + } + } + foreach ( $vars as $k => $v ) { + $path_new = $path; + $path_new[] = $k; + $v_new = is_array($v) || is_object($v) + ? $this->removeReferences($v, $hist, $path_new) + : $v; + if ( $type == 'array' ) { + unset($a[$k]); + $a[$k] = $v_new; } else { - foreach ( $v as $k => $v2 ) { - $path_new = $path; - $path_new[] = $k; - unset($v[$k]); // remove any reference - $v[$k] = is_array($v2) - ? $this->removeReferences($v2, $path_new) - : $v2; - } + unset($a_clone->{$k}); + $a_clone->{$k} = $v_new; } } - return $v; + if ( $type == 'object' && isset($a_clone) ) { + $a = $a_clone; + } + return $a; } /** @@ -1969,4 +2135,4 @@ function toUtf8($str) } -?> +?> \ No newline at end of file diff --git a/README.md b/README.md index c9c85ec1..391e3cd4 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ Browser/javascript like console class for PHP **Website/Usage/Examples:** http://www.bradkent.com/?page=php/debug * PHP port of the [javascript web console api](https://developer.mozilla.org/en-US/docs/Web/API/console) -* abstracts/wraps [FirePHP](http://www.firephp.org/) +* abstracts/wraps [FirePHP](http://www.firephp.org/) * custom error handler ![Screenshot of PHPDebugConsole's Output](http://www.bradkent.com/images/bradkent.com/php/screenshot.png) @@ -25,4 +25,4 @@ Browser/javascript like console class for PHP * table * time * timeEnd -* *... more* +* *... [more](http://www.bradkent.com/?page=php/debug#docs-methods)*