Created
June 9, 2012 16:27
-
-
Save JosephLenton/2901668 to your computer and use it in GitHub Desktop.
pretty errors (as is)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<? | |
/** | |
* Pretty Errors | |
* | |
* ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### | |
* | |
* WARNING! It is downright _DANGEROUS_ to use this in production, on | |
* a live website. It should *ONLY* be used for development. | |
* | |
* Pretty Errors allows the outside world to alter your project, | |
* and this is deliberate (it's what makes pretty errors so powerful). | |
* It deliberately displays as much information about your runtime as | |
* possible, and errors are free to perform HTML injection. | |
* | |
* If you use it in development, awesome! If you use it in production, | |
* you're an idiot. | |
* | |
* ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### | |
* | |
* A small API for replacing the standard PHP errors, with prettier | |
* error reporting. | |
* | |
* Basic example: | |
* | |
* \pretty_errors\reportErrors(); | |
* | |
* ... and that's it! There is more too it if you want more customized | |
* error handling. | |
* | |
* Fuller example: | |
* | |
* $handler = new \pretty_errors\PrettyErrorsHandler( $myOptions ); | |
* | |
* There should only ever be one handler! This is an (underdstandable) | |
* limitation in PHP. It's because if an exception or error is raised, | |
* then there is a single point of handling it. | |
* | |
* @author Joseph Lenton | https://github.com/josephlenton | |
*/ | |
namespace pretty_errors; | |
use \pretty_errors\PrettyErrorsHandler; | |
use \ReflectionMethod, | |
\ReflectionFunction, | |
\ReflectionParameter; | |
use \stdClass as stdClass, | |
\Exception; | |
global $_pretty_errors_global_handler; | |
$_pretty_errors_global_handler = null; | |
/** | |
* | |
*/ | |
function updateFile( $name, $content ) | |
{ | |
file_put_contents( $name, $content ); | |
} | |
/** | |
* Turns on error reporting, and returns the handler. | |
* | |
* If you just want error reporting on, then don't bother | |
* catching the handler. If you're building something | |
* clever, like a framework, then you might want to grab | |
* and use it. | |
* | |
* Note that you can only call this *once*. Repeat calls | |
* will throw an exception. | |
* | |
* @param options Optional, options declaring how pretty errors should be setup and used. | |
* @return The PrettyErrorsHandler used for reporting errors. | |
*/ | |
function reportErrors( $options=null ) { | |
$handler = new PrettyErrorsHandler( $options ); | |
return $handler->turnOn(); | |
} | |
class PrettyErrorsHandler extends stdClass | |
{ | |
const REGEX_PHP_IDENTIFIER = '\b[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*'; | |
/** | |
* Matches: | |
* blah::foo() | |
* foo() | |
*/ | |
const REGEX_METHOD_OR_FUNCTION_END = '/\b[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*(::[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*)?\\(\\)$/'; | |
const REGEX_METHOD_OR_FUNCTION = '/\b[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*(::[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*)?\\(\\)/'; | |
/** | |
* The number of lines to take from the file, | |
* where the error is reported. This is the number | |
* of lines around the line in question, | |
* including that line. | |
* | |
* So '9' will be the error line + 4 lines above + 4 lines below. | |
*/ | |
const NUM_FILE_LINES = 7; | |
const FILE_TYPE_APPLICATION = 1; | |
const FILE_TYPE_IGNORE = 2; | |
/** | |
* At the time of writing, scalar type hints are unsupported. | |
* By scalar, I mean 'string' and 'integer'. | |
* | |
* If they do get added, this is here as a trap to turn scalar | |
* type hint warnings on and off. | |
*/ | |
private static $IS_SCALAR_TYPE_HINTING_SUPPORTED = false; | |
private static $SCALAR_TYPES = array( | |
'string', 'integer', 'float', 'boolean', 'int', 'number' | |
); | |
/** | |
* A mapping of PHP internal symbols, | |
* mapped to descriptions of them. | |
*/ | |
private static $PHP_SYMBOL_MAPPINGS = array( | |
'$end' => 'end of file', | |
'T_ABSTRACT' => 'abstract', | |
'T_AND_EQUAL' => '&=', | |
'T_ARRAY' => 'array', | |
'T_ARRAY_CAST' => 'array cast', | |
'T_AS' => 'as', | |
'T_BOOLEAN_AND' => '&&', | |
'T_BOOLEAN_OR' => '||', | |
'T_BOOL_CAST' => 'boolean cast', | |
'T_BREAK' => 'break', | |
'T_CASE' => 'case', | |
'T_CATCH' => 'catch', | |
'T_CLASS' => 'class', | |
'T_CLASS_C' => '__CLASS__', | |
'T_CLONE' => 'clone', | |
'T_CLOSE_TAG' => 'closing PHP tag', | |
'T_CONCAT_EQUAL' => '.=', | |
'T_CONST' => 'const', | |
'T_CONSTANT_ENCAPSED_STRING' => 'string', | |
'T_CONTINUE' => 'continue', | |
'T_DEC' => '-- decrement', | |
'T_DECLARE' => 'declare', | |
'T_DEFAULT' => 'default', | |
'T_DIR' => '__DIR__', | |
'T_DIV_EQUAL' => '/=', | |
'T_DNUMBER' => 'number', | |
'T_DO' => 'do', | |
'T_DOUBLE_ARROW' => '=>', | |
'T_DOUBLE_CAST' => 'double cast', | |
'T_DOUBLE_COLON' => '::', | |
'T_ECHO' => 'echo', | |
'T_ELSE' => 'else', | |
'T_ELSEIF' => 'elseif', | |
'T_EMPTY' => 'empty', | |
'T_ENDDECLARE' => 'enddeclare', | |
'T_ENDFOR' => 'endfor', | |
'T_ENDFOREACH' => 'endforeach', | |
'T_ENDIF' => 'endif', | |
'T_ENDSWITCH' => 'endswitch', | |
'T_ENDWHILE' => 'endwhile', | |
'T_EVAL' => 'eval', | |
'T_EXIT' => 'exit call', | |
'T_EXTENDS' => 'extends', | |
'T_FILE' => '__FILE__', | |
'T_FINAL' => 'final', | |
'T_FOR' => 'for', | |
'T_FOREACH' => 'foreach', | |
'T_FUNCTION' => 'function', | |
'T_FUNC_C' => '__FUNCTION__', | |
'T_GLOBAL' => 'global', | |
'T_GOTO' => 'goto', | |
'T_HALT_COMPILER' => '__halt_compiler', | |
'T_IF' => 'if', | |
'T_IMPLEMENTS' => 'implements', | |
'T_INC' => '++ increment', | |
'T_INCLUDE' => 'include', | |
'T_INCLUDE_ONCE' => 'include_once', | |
'T_INSTANCEOF' => 'instanceof', | |
'T_INT_CAST' => 'int cast', | |
'T_INTERFACE' => 'interface', | |
'T_ISSET' => 'isset', | |
'T_IS_EQUAL' => '==', | |
'T_IS_GREATER_OR_EQUAL' => '>=', | |
'T_IS_IDENTICAL' => '===', | |
'T_IS_NOT_EQUAL' => '!= or <>', | |
'T_IS_NOT_IDENTICAL' => '!==', | |
'T_IS_SMALLER_OR_EQUAL' => '<=', | |
'T_LINE' => '__LINE__', | |
'T_LIST' => 'list', | |
'T_LNUMBER' => 'number', | |
'T_LOGICAL_AND' => 'and', | |
'T_LOGICAL_OR' => 'or', | |
'T_LOGICAL_XOR' => 'xor', | |
'T_METHOD_C' => '__METHOD__', | |
'T_MINUS_EQUAL' => '-=', | |
'T_MOD_EQUAL' => '%=', | |
'T_MUL_EQUAL' => '*=', | |
'T_NAMESPACE' => 'namespace', | |
'T_NS_C' => '__NAMESPACE__', | |
'T_NS_SEPARATOR' => '/ namespace seperator', | |
'T_NEW' => 'new', | |
'T_OBJECT_CAST' => 'object cast', | |
'T_OBJECT_OPERATOR' => '->', | |
'T_OLD_FUNCTION' => 'old_function', | |
'T_OPEN_TAG' => '<?php or <?', | |
'T_OPEN_TAG_WITH_ECHO' => '<?=', | |
'T_OR_EQUAL' => '|=', | |
'T_PAAMAYIM_NEKUDOTAYIM' => '::', | |
'T_PLUS_EQUAL' => '+=', | |
'T_PRINT' => 'print', | |
'T_PRIVATE' => 'private', | |
'T_PUBLIC' => 'public', | |
'T_PROTECTED' => 'protected', | |
'T_REQUIRE' => 'require', | |
'T_REQUIRE_ONCE' => 'require_once', | |
'T_RETURN' => 'return', | |
'T_SL' => '<<', | |
'T_SL_EQUAL' => '<<=', | |
'T_SR' => '>>', | |
'T_SR_EQUAL' => '>>=', | |
'T_START_HEREDOC' => '<<<', | |
'T_STATIC' => 'static', | |
'T_STRING' => 'string', | |
'T_STRING_CAST' => 'string cast', | |
'T_SWITCH' => 'switch', | |
'T_THROW' => 'throw', | |
'T_TRY' => 'try', | |
'T_UNSET' => 'unset', | |
'T_UNSET_CAST' => 'unset cast', | |
'T_USE' => 'use', | |
'T_VAR' => 'var', | |
'T_VARIABLE' => 'variable', | |
'T_WHILE' => 'while', | |
'T_XOR_EQUAL' => '^=' | |
); | |
private static $syntaxMap = array( | |
T_COMMENT => 'syntax-comment', | |
'reference_ampersand' => 'syntax-function', | |
T_ABSTRACT => 'syntax-keyword', | |
T_AS => 'syntax-keyword', | |
T_BREAK => 'syntax-keyword', | |
T_CASE => 'syntax-keyword', | |
T_CATCH => 'syntax-keyword', | |
T_CLASS => 'syntax-keyword', | |
T_CONST => 'syntax-keyword', | |
T_CONTINUE => 'syntax-keyword', | |
T_DECLARE => 'syntax-keyword', | |
T_DEFAULT => 'syntax-keyword', | |
T_DO => 'syntax-keyword', | |
T_ELSE => 'syntax-keyword', | |
T_ELSEIF => 'syntax-keyword', | |
T_ENDDECLARE => 'syntax-keyword', | |
T_ENDFOR => 'syntax-keyword', | |
T_ENDFOREACH => 'syntax-keyword', | |
T_ENDIF => 'syntax-keyword', | |
T_ENDSWITCH => 'syntax-keyword', | |
T_ENDWHILE => 'syntax-keyword', | |
T_EXTENDS => 'syntax-keyword', | |
T_FINAL => 'syntax-keyword', | |
T_FOR => 'syntax-keyword', | |
T_FOREACH => 'syntax-keyword', | |
T_FUNCTION => 'syntax-keyword', | |
T_GLOBAL => 'syntax-keyword', | |
T_GOTO => 'syntax-keyword', | |
T_IF => 'syntax-keyword', | |
T_IMPLEMENTS => 'syntax-keyword', | |
T_INSTANCEOF => 'syntax-keyword', | |
T_INTERFACE => 'syntax-keyword', | |
T_LOGICAL_AND => 'syntax-keyword', | |
T_LOGICAL_OR => 'syntax-keyword', | |
T_LOGICAL_XOR => 'syntax-keyword', | |
T_NAMESPACE => 'syntax-keyword', | |
T_NEW => 'syntax-keyword', | |
T_PRIVATE => 'syntax-keyword', | |
T_PUBLIC => 'syntax-keyword', | |
T_PROTECTED => 'syntax-keyword', | |
T_RETURN => 'syntax-keyword', | |
T_STATIC => 'syntax-keyword', | |
T_SWITCH => 'syntax-keyword', | |
T_THROW => 'syntax-keyword', | |
T_TRY => 'syntax-keyword', | |
T_USE => 'syntax-keyword', | |
T_VAR => 'syntax-keyword', | |
T_WHILE => 'syntax-keyword', | |
// __VAR__ type magic constants | |
T_CLASS_C => 'syntax-literal', | |
T_DIR => 'syntax-literal', | |
T_FILE => 'syntax-literal', | |
T_FUNC_C => 'syntax-literal', | |
T_LINE => 'syntax-literal', | |
T_METHOD_C => 'syntax-literal', | |
T_NS_C => 'syntax-literal', | |
T_DNUMBER => 'syntax-literal', | |
T_LNUMBER => 'syntax-literal', | |
T_CONSTANT_ENCAPSED_STRING => 'syntax-string', | |
T_VARIABLE => 'syntax-variable', | |
// this is for unescaped strings, which appear differently | |
// this includes function names | |
T_STRING => 'syntax-function', | |
// in build keywords, which work like functions | |
T_ARRAY => 'syntax-function', | |
T_CLONE => 'syntax-function', | |
T_ECHO => 'syntax-function', | |
T_EMPTY => 'syntax-function', | |
T_EVAL => 'syntax-function', | |
T_EXIT => 'syntax-function', | |
T_HALT_COMPILER => 'syntax-function', | |
T_INCLUDE => 'syntax-function', | |
T_INCLUDE_ONCE => 'syntax-function', | |
T_ISSET => 'syntax-function', | |
T_LIST => 'syntax-function', | |
T_REQUIRE_ONCE => 'syntax-function', | |
T_PRINT => 'syntax-function', | |
T_REQUIRE => 'syntax-function', | |
T_UNSET => 'syntax-function' | |
); | |
/** | |
* Looks up a description for the symbol given, | |
* and if found, it is returned. | |
* | |
* If it's not found, then the symbol given is returned. | |
*/ | |
private static function phpSymbolToDescription( $symbol ) { | |
if ( isset(PrettyErrorsHandler::$PHP_SYMBOL_MAPPINGS[$symbol]) ) { | |
return PrettyErrorsHandler::$PHP_SYMBOL_MAPPINGS[$symbol]; | |
} else { | |
return $symbol; | |
} | |
} | |
/** | |
* Attempts to syntax highlight the code snippet done. | |
* | |
* This is then returned as HTML, ready to be dumped to the screen. | |
* | |
* @param code An array of code lines to syntax highlight. | |
* @return HTML version of the code given, syntax highlighted. | |
*/ | |
private static function syntaxHighlight( $code ) { | |
$syntaxMap = PrettyErrorsHandler::$syntaxMap; | |
// @supress invalid code raises a warning | |
$tokens = @token_get_all( "<?php " . $code . " ?>" ); | |
$html = array(); | |
$len = count($tokens)-1; | |
$inString = false; | |
$stringBuff = null; | |
$skip = false; | |
for ( $i = 1; $i < $len; $i++ ) { | |
$token = $tokens[$i]; | |
if ( is_array($token) ) { | |
$type = $token[0]; | |
$code = $token[1]; | |
if ( $type === T_CONSTANT_ENCAPSED_STRING ) { | |
$code = htmlspecialchars( $code ); | |
} | |
} else { | |
$type = null; | |
$code = $token; | |
} | |
// work out any whitespace padding | |
if ( strpos($code, "\n") !== false && trim($code) === '' ) { | |
if ( $inString ) { | |
$html[]= "<span class='syntax-string'>" . join('', $stringBuff); | |
$stringBuff = array(); | |
} | |
} else if ( $code === '&' ) { | |
if ( $i < $len ) { | |
$next = $tokens[$i+1]; | |
if ( is_array($next) && $next[0] === T_VARIABLE ) { | |
$code = '&'; | |
$type = 'reference_ampersand'; | |
} | |
} | |
} else if ( $code === '"' || $code === "'" ) { | |
if ( $inString ) { | |
$html[]= "<span class='syntax-string'>" . join('', $stringBuff) . "$code</span>"; | |
$stringBuff = null; | |
$skip = true; | |
} else { | |
$stringBuff = array(); | |
} | |
$inString = !$inString; | |
} else if ( $code === '->' ) { | |
$code = '->'; | |
} | |
if ( $skip ) { | |
$skip = false; | |
} else { | |
if ( $type !== null && isset($syntaxMap[$type]) ) { | |
$class = $syntaxMap[$type]; | |
if ( $type === T_CONSTANT_ENCAPSED_STRING && strpos($code, "\n") !== false ) { | |
$append = "<span class='$class'>" . | |
join( | |
"</span>\n<span class='$class'>", | |
explode( "\n", $code ) | |
) . | |
"</span>" ; | |
} else { | |
$append = "<span class='$class'>$code</span>"; | |
} | |
} else if ( $inString && $code !== '"' ) { | |
$append = "<span class='syntax-string'>$code</span>"; | |
} else { | |
$append = $code; | |
} | |
if ( $inString ) { | |
$stringBuff[]= $append; | |
} else { | |
$html[]= $append; | |
} | |
} | |
} | |
if ( $stringBuff !== null ) { | |
$html[]= "<span class='syntax-string'>" . join('', $stringBuff) . '</span>'; | |
$stringBuff = null; | |
} | |
return join( '', $html ); | |
} | |
/** | |
* Splits a given function name into it's 'class, function' parts. | |
* If there is no class, then null is returned. | |
* | |
* It also returns these parts in an array of: array( $className, $functionName ); | |
* | |
* Usage: | |
* | |
* list( $class, $function ) = \pretty_errors\PrettyErrorsHandler::splitFunction( $name ); | |
* | |
* @param name The function name to split. | |
* @return An array containing class and function name. | |
*/ | |
private static function splitFunction( $name ) { | |
$name = preg_replace( '/\\(\\)$/', '', $name ); | |
if ( strpos($name, '::') !== false ) { | |
$parts = explode( '::', $name ); | |
$className = $parts[0]; | |
$type = '::'; | |
$functionName = $parts[1]; | |
} else if ( strpos($name, '->') !== false ) { | |
$parts = explode( '->', $name ); | |
$className = $parts[0]; | |
$type = '->'; | |
$functionName = $parts[1]; | |
} else { | |
$className = null; | |
$type = null; | |
$functionName = $name; | |
} | |
return array( $className, $type, $functionName ); | |
} | |
private static function newArgument( $name, $type=false, $isPassedByReference=false, $isOptional=false, $optionalValue=null, $highlight=false ) { | |
if ( $name instanceof ReflectionParameter ) { | |
$highlight = func_num_args() > 1 ? | |
$highlight = $type : | |
false; | |
$klass = $name->getDeclaringClass(); | |
$functionName = $name->getDeclaringFunction()->name; | |
if ( $klass !== null ) { | |
$klass = $klass->name; | |
} | |
$export = ReflectionParameter::export( | |
( $klass ? | |
array( "\\$klass", $functionName ) : | |
$functionName ), | |
$name->name, | |
true | |
); | |
$paramType = preg_replace('/.*?(\w+)\s+\$'.$name->name.'.*/', '\\1', $export); | |
if ( strpos($paramType, '[') !== false || strlen($paramType) === 0 ) { | |
$paramType = null; | |
} | |
return PrettyErrorsHandler::newArgument( | |
$name->name, | |
$paramType, | |
$name->isPassedByReference(), | |
$name->isDefaultValueAvailable(), | |
( $name->isDefaultValueAvailable() ? | |
var_export( $name->getDefaultValue(), true ) : | |
null ), | |
( func_num_args() > 1 ? | |
$type : | |
false ) | |
); | |
} else { | |
return array( | |
'name' => $name, | |
'has_type' => ( $type !== false ), | |
'type' => $type, | |
'is_reference' => $isPassedByReference, | |
'has_default' => $isOptional, | |
'default_val' => $optionalValue, | |
'is_highlighted' => $highlight | |
); | |
} | |
} | |
private static function syntaxHighlightFunctionMatch( $match, &$stackTrace, $highlightArg=null, &$numHighlighted=0 ) { | |
list( $className, $type, $functionName ) = PrettyErrorsHandler::splitFunction( $match ); | |
// is class::method() | |
if ( $className !== null ) { | |
$reflectFun = new ReflectionMethod( $className, $functionName ); | |
// is a function | |
} else { | |
$reflectFun = new ReflectionFunction( $functionName ); | |
} | |
$params = $reflectFun->getParameters(); | |
if ( $params ) { | |
$args = array(); | |
$min = 0; | |
foreach( $params as $i => $param ) { | |
$arg = PrettyErrorsHandler::newArgument( $param ); | |
if ( ! $arg['has_default'] ) { | |
$min = $i; | |
} | |
$args[]= $arg; | |
} | |
if ( $highlightArg !== null ) { | |
for ( $i = $highlightArg; $i <= $min; $i++ ) { | |
$args[$i]['is_highlighted'] = true; | |
} | |
$numHighlighted = $min-$highlightArg; | |
} | |
if ( $className !== null ) { | |
if ( $stackTrace && isset($stackTrace[1]) && isset($stackTrace[1]['type']) ) { | |
$type = htmlspecialchars( $stackTrace[1]['type'] ); | |
} | |
} else { | |
$type = null; | |
} | |
return PrettyErrorsHandler::syntaxHighlightFunction( $className, $type, $functionName, $args ); | |
} else { | |
return null; | |
} | |
} | |
/** | |
* Returns the values given, as HTML, syntax highlighted. | |
* It's a shorter, slightly faster, more no-nonsense approach | |
* then 'syntaxHighlight'. | |
* | |
* This is for syntax highlighting: | |
* - fun( [args] ) | |
* - class->fun( [args] ) | |
* - class::fun( [args] ) | |
* | |
* Class and type can be null, to denote no class, but are not optional. | |
*/ | |
private static function syntaxHighlightFunction( $class, $type, $fun, &$args=null ) { | |
$info = array(); | |
// set the info | |
if ( isset($class) && $class && isset($type) && $type ) { | |
if ( $type === '->' ) { | |
$type = '->'; | |
} | |
$info []= "<span class='syntax-class'>$class</span>$type"; | |
} | |
if ( isset($fun) && $fun ) { | |
$info []= "<span class='syntax-function'>$fun</span>"; | |
} | |
if ( $args ) { | |
$info []= '( '; | |
foreach ($args as $i => $arg) { | |
if ( $i > 0 ) { | |
$info[]= ', '; | |
} | |
if ( is_string($arg) ) { | |
$info[]= $arg; | |
} else { | |
$highlight = $arg['is_highlighted']; | |
$name = $arg['name']; | |
if ( $highlight ) { | |
$info[]= '<span class="syntax-higlight-variable">'; | |
} | |
if ( $name === '_' ) { | |
$info[]= '<span class="syntax-variable-not-important">'; | |
} | |
if ( $arg['has_type'] ) { | |
$info []= "<span class='syntax-class'>"; | |
$info []= $arg['type']; | |
$info []= '</span> '; | |
} | |
if ( $arg['is_reference'] ) { | |
$info []= '<span class="syntax-function">&</span>'; | |
} | |
$info []= "<span class='syntax-variable'>\$$name</span>"; | |
if ( $arg['has_default'] ) { | |
$info []= '=<span class="syntax-literal">' . $arg['default_val'] . '</span>'; | |
} | |
if ( $name === '_' ) { | |
$info[]= '</span>'; | |
} | |
if ( $highlight ) { | |
$info[]= '</span>'; | |
} | |
} | |
} | |
$info []= ' )'; | |
} else { | |
$info []= '()'; | |
} | |
return join( '', $info ); | |
} | |
/** | |
* Checks if the item is in options, and if it is, then it is removed and returned. | |
* | |
* If it is not found, or if options is not an array, then the alt is returned. | |
*/ | |
private static function optionsPop( &$options, $key, $alt=null ) { | |
if ( $options && isset($options[$key]) ) { | |
$val = $options[$key]; | |
unset( $options[$key] ); | |
return $val; | |
} else { | |
return $alt; | |
} | |
} | |
private static function isFolderType( &$folders, $longest, $file ) { | |
$parts = explode( '/', $file ); | |
$len = min( count($parts), $longest ); | |
for ( $i = 0; $i < $len; $i++ ) { | |
if ( isset($folders[$i+1]) ) { | |
$folderParts = &$folders[ $i+1 ]; | |
$success = false; | |
for ( $j = 0; $j < count($folderParts); $j++ ) { | |
if ( $folderParts[$j] === $parts[$j] ) { | |
$success = true; | |
} else { | |
$success = false; | |
break; | |
} | |
} | |
if ( $success ) { | |
return true; | |
} | |
} | |
} | |
return false; | |
} | |
private static function setFolders( &$origFolders, &$longest, $folders ) { | |
$newFolders = array(); | |
$newLongest = 0; | |
foreach ( $folders as $i => $folder ) { | |
$folder = str_replace( '\\', '/', $folder ); | |
$folder = preg_replace( '/(^\\/+)|(\\/+$)/', '', $folder ); | |
$parts = explode( '/', $folder ); | |
$count = count( $parts ); | |
$newLongest = max( $newLongest, $count ); | |
if ( isset($newFolders[$count]) ) { | |
$folds = &$newFolders[$count]; | |
$folds[]= $folder; | |
} else { | |
$newFolders[$count] = array( $folder ); | |
} | |
} | |
$origFolders = $newFolders; | |
$longest = $newLongest; | |
} | |
private $cachedFiles; | |
private $isShutdownRegistered; | |
private $isOn; | |
private $ignoreFolders = array(); | |
private $ignoreFoldersLongest = 0; | |
private $applicationFolders = array(); | |
private $applicationFoldersLongest = 0; | |
private $defaultErrorReportingOn; | |
private $defaultErrorReportingOff; | |
private $documentRoot; | |
private $serverName; | |
private $catchClassNotFound; | |
private $catchSurpressedErrors; | |
private $backgroundText; | |
/** | |
* = Options = | |
* | |
* All options are optional, and so is passing in an options item. | |
* You don't have to supply any, it's up to you. | |
* | |
* Includes: | |
* = Types of errors this will / won't catch = | |
* - catch_class_not_found When true, calling the class autoloader in PHP will throw an error. This defaults to true. | |
* - catch_supressed_errors The @ supresses errors. However if this is set to true, then they are still reported. Defaults to false. | |
* | |
* = Error reporting level = | |
* - error_reporting_on value for when errors are on, defaults to all errors | |
* - error_reporting_off value for when errors are off, defaults to php.ini's error_reporting. | |
* | |
* = Setup details = | |
* - document_root When it's working out hte stack trace, this is the root folder of the application, to use as it's base. | |
* Defaults to the servers root directory. | |
* | |
* - server_name The name for this server, defaults to "$_SERVER['SERVER_NAME']" | |
* | |
* - ignore_folders This is allows you to highlight non-framework code in a stack trace. | |
* An array of folders to ignore, when working out the stack trace. | |
* This is folder prefixes in relation to the document_root, whatever that might be. | |
* They are only ignored if there is a file found outside of them. | |
* If you still don't get what this does, don't worry, it's here cos I use it. | |
* | |
* - application_folders Just like ignore, but anything found in these folders takes precedence | |
* over anything else. | |
* | |
* - background_text The text that appeares in the background. By default it says 'ERROR!'. | |
* Why? You can replace this with the name of your framework, for extra customization spice. | |
* | |
* @param options Optional, an array of values to customize this handler. | |
* @throws Exception This is raised if given an options that does *not* exist (so you know that option is meaningless). | |
*/ | |
public function __construct( $options=null ) { | |
// there can only be one to rule them all | |
global $_pretty_errors_global_handler; | |
if ( $_pretty_errors_global_handler !== null ) { | |
throw new Exception( "there can only ever be one PrettyErrorsHandler" ); | |
} | |
$_pretty_errors_global_handler = $this; | |
$this->cachedFiles = array(); | |
$this->isShutdownRegistered = false; | |
$this->isOn = false; | |
/* | |
* Deal with the options. | |
* | |
* They are removed one by one, and any left, will raise an error. | |
*/ | |
$ignoreFolders = PrettyErrorsHandler::optionsPop( $options, 'ignore_folders' , null ); | |
if ( $ignoreFolders !== null ) { | |
$this->setFolders( $this->ignoreFolders, $this->ignoreFoldersLongest, $ignoreFolders ); | |
} | |
$appFolders = PrettyErrorsHandler::optionsPop( $options, 'application_folders' , null ); | |
if ( $appFolders !== null ) { | |
$this->setFolders( $this->applicationFolders, $this->applicationFoldersLongest, $appFolders ); | |
} | |
$this->defaultErrorReportingOn = PrettyErrorsHandler::optionsPop( $options, 'error_reporting_on' , -1 ); | |
$this->defaultErrorReportingOff = PrettyErrorsHandler::optionsPop( $options, 'error_reporting_off', error_reporting() ); | |
$this->documentRoot = str_replace( '\\', '/', | |
PrettyErrorsHandler::optionsPop( $options, 'document_root', $_SERVER['DOCUMENT_ROOT'] ) | |
); | |
$this->serverName = PrettyErrorsHandler::optionsPop( $options, 'error_reporting_off', $_SERVER['SERVER_NAME'] ); | |
$this->catchClassNotFound = PrettyErrorsHandler::optionsPop( $options, 'catch_class_not_found' , true ); | |
$this->catchSurpressedErrors = PrettyErrorsHandler::optionsPop( $options, 'catch_supressed_errors', false ); | |
$this->backgroundText = PrettyErrorsHandler::optionsPop( $options, 'background_text', 'ERROR!' ); | |
if ( $options ) { | |
foreach ( $options as $key => $val ) { | |
throw new \Exception( "Unknown option given $key" ); | |
} | |
} | |
} | |
public function isOn() { | |
return $this->isOn; | |
} | |
public function isOff() { | |
return !$this->isOn; | |
} | |
public function turnOn() { | |
$this->isOn = true; | |
$this->attachErrorHandles(); | |
return $this; | |
} | |
public function turnOff() { | |
$this->isOn = false; | |
return $this; | |
} | |
/** | |
* Allows you to run a callback with strict errors turned off. | |
* Standard errors still apply, but this will use the default | |
* error and exception handlers. | |
* | |
* This is useful for when loading libraries which do not | |
* adhere to strict errors, such as Wordpress. | |
* | |
* To use: | |
* | |
* withoutErrors( function() { | |
* // unsafe code here | |
* }); | |
* | |
* This will use the error_reporting value from the php.ini | |
* file, and failing that, it will use 'E_ALL & ~E_DEPRECATED'. | |
* | |
* @param callback A PHP function to call. | |
*/ | |
public function withoutErrors( $callback ) { | |
if ( ! is_callable($callback) ) { | |
throw new Exception( "non callable callback given" ); | |
} | |
$this->turnOff(); | |
$callback(); | |
$this->turnOn(); | |
return $this; | |
} | |
private function isApplicationFolder( $file ) { | |
return PrettyErrorsHandler::isFolderType( | |
$this->applicationFolders, | |
$this->applicationFoldersLongest, | |
$file | |
); | |
} | |
private function isIgnoreFolder( $file ) { | |
return PrettyErrorsHandler::isFolderType( | |
$this->ignoreFolders, | |
$this->ignoreFoldersLongest, | |
$file | |
); | |
} | |
private function getFolderType( $root, $file=null ) { | |
if ( func_num_args() === 1 ) { | |
$file = $root; | |
} else { | |
$file = $this->removeRootPath( $root, $file ); | |
} | |
if ( $this->isApplicationFolder($file) ) { | |
return PrettyErrorsHandler::FILE_TYPE_APPLICATION; | |
} else if ( $this->isIgnoreFolder($file) ) { | |
return PrettyErrorsHandler::FILE_TYPE_IGNORE; | |
} else { | |
return false; | |
} | |
} | |
/** | |
* Finds the file named, and returns it's contents in an array. | |
* | |
* It's essentially the same as 'file_get_contents'. However | |
* this will add caching at this PHP layer, avoiding lots of | |
* duplicate calls. | |
* | |
* It also splits the file into an array of lines, and makes | |
* it html safe. | |
* | |
* @param path The file to get the contents of. | |
* @return The file we are after, as an array of lines. | |
*/ | |
private function getFileContents( $path ) { | |
if ( isset($this->cachedFiles[$path]) ) { | |
return $this->cachedFiles[$path]; | |
} else { | |
$contents = explode( | |
"\n", | |
preg_replace( | |
'/(\r\n)|(\n\r)|\r/', | |
"\n", | |
file_get_contents($path) | |
) | |
); | |
$this->cachedFiles[ $path ] = $contents; | |
return $contents; | |
} | |
} | |
/** | |
* Reads out the code from the section of the line, | |
* which is at fault. | |
* | |
* The array is in a mapping of: array( line-number => line ) | |
* | |
* If something goes wrong, then null is returned. | |
*/ | |
private function readCodeFile( $errFile, $errLine ) { | |
try { | |
$lines = $this->getFileContents( $errFile ); | |
$numLines = PrettyErrorsHandler::NUM_FILE_LINES; | |
/* | |
* This ensures we attempt to always get NUM_FILE_LINES | |
* number of lines, if we are at the top of the file, | |
* for consistency. | |
*/ | |
$searchDown = (int)( $numLines/2 ); | |
$minLine = max( 0, $errLine-$searchDown ); | |
$maxLine = min( $minLine+$numLines, count($lines) ); | |
$fileLines = array_splice( $lines, $minLine, $maxLine-$minLine ); | |
$fileLines = join( "\n", $fileLines ); | |
$fileLines = PrettyErrorsHandler::syntaxHighlight( $fileLines ); | |
$fileLines = explode( "\n", $fileLines ); | |
$lines = array(); | |
for ( $i = 0; $i < count($fileLines); $i++ ) { | |
// +1 is because line numbers start at 1, whilst arrays start at 0 | |
$lines[ $i+$minLine+1 ] = $fileLines[$i]; | |
} | |
return $lines; | |
} catch ( Exception $ex ) { | |
return null; | |
} | |
return null; | |
} | |
/** | |
* Attempts to remove the root path from the path given. | |
* If the path can't be removed, then the original path is returned. | |
* | |
* For example if root is 'C:/users/projects/my_site', | |
* and the file is 'C:/users/projects/my_site/index.php', | |
* then the root is removed, and we are left with just 'index.php'. | |
* | |
* This is to remove line noise; you don't need to be told the | |
* 'C:/whatever' bit 20 times. | |
* | |
* @param root The root path to remove. | |
* @param path The file we are removing the root section from. | |
*/ | |
private function removeRootPath( $root, $path ) { | |
$filePath = str_replace( '\\', '/', $path ); | |
if ( | |
strpos($filePath, $root) === 0 && | |
strlen($root) < strlen($filePath) | |
) { | |
return substr($filePath, strlen($root)+1 ); | |
} else { | |
return $filePath; | |
} | |
} | |
/** | |
* Parses, and alters, the errLine, errFile and message given. | |
* | |
* This includes adding syntax highlighting, removing duplicate | |
* information we already have, and making the error easier to | |
* read. | |
*/ | |
private function improveErrorMessage( $ex, $code, $message, $errLine, $errFile, $root, &$stackTrace ) { | |
// change these to change where the source file is come from | |
$srcErrFile = $errFile; | |
$srcErrLine = $errLine; | |
$altInfo = null; | |
$stackSearchI = 0; | |
$skipStackFirst = function( &$stackTrace ) { | |
$skipFirst = true; | |
foreach ( $stackTrace as $i => $trace ) { | |
if ( $skipFirst ) { | |
$skipFirst = false; | |
} else { | |
if ( $trace && isset($trace['file']) && isset($trace['line']) ) { | |
return array( $trace['file'], $trace['line'], $i ); | |
} | |
} | |
} | |
return array( null, null, null ); | |
}; | |
/* | |
* This is for calling a function that doesn't exist. | |
* | |
* The message contains a long description of where this takes | |
* place, even though we are already told this through line and | |
* file info. So we cut it out. | |
*/ | |
if ( $code === 1 ) { | |
if ( | |
( strpos($message, " undefined method ") !== false ) || | |
( strpos($message, " undefined function ") !== false ) | |
) { | |
$matches = array(); | |
preg_match( '/\b[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*((->|::)[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*)?\\(\\)$/', $message, $matches ); | |
/* | |
* undefined function or method call | |
*/ | |
if ( $matches ) { | |
list( $className, $type, $functionName ) = PrettyErrorsHandler::splitFunction( $matches[0] ); | |
$args = array(); | |
if ( $ex && $ex instanceof \pretty_errors\CallErrorToExceptionException ) { | |
$numArgs = $ex->getNumArgs(); | |
for ( $i = 0; $i < $numArgs; $i++ ) { | |
$args[]= PrettyErrorsHandler::newArgument( "_" ); | |
} | |
} | |
$message = preg_replace( | |
'/\b[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*((->|::)[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*)?\\(\\)$/', | |
PrettyErrorsHandler::syntaxHighlightFunction( $className, $type, $functionName, $args ), | |
$message | |
); | |
} | |
} else if ( | |
strpos($message, "Class ") !== false && | |
strpos($message, "not found") !== false | |
) { | |
$matches = array(); | |
preg_match( '/\'(\\\\)?[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*((\\\\)?[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*)+\'/', $message, $matches ); | |
if ( count($matches) > 0 ) { | |
// lose the 'quotes' | |
$className = $matches[0]; | |
$className = substr( $className, 1, strlen($className)-2 ); | |
$message = preg_replace( | |
'/\'(\\\\)?[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*((\\\\)?[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*)+\'/', | |
"'<span class='syntax-class'>$className</span>'", | |
$message | |
); | |
} | |
} | |
} else if ( $code === 2 ) { | |
if ( strpos($message, "Missing argument ") === 0 ) { | |
$message = preg_replace( '/, called in .*$/', '', $message ); | |
$matches = array(); | |
preg_match( PrettyErrorsHandler::REGEX_METHOD_OR_FUNCTION_END, $message, $matches ); | |
if ( $matches ) { | |
$argumentMathces = array(); | |
preg_match( '/^Missing argument ([0-9]+)/', $message, $argumentMathces ); | |
$highlightArg = count($argumentMathces) === 2 ? | |
(((int) $argumentMathces[1])-1) : | |
null ; | |
$altInfo = PrettyErrorsHandler::syntaxHighlightFunctionMatch( $matches[0], $stackTrace, $highlightArg, $numHighlighted ); | |
if ( $numHighlighted > 0 ) { | |
$message = preg_replace( '/^Missing argument ([0-9]+)/', 'Missing arguments ', $message ); | |
} | |
if ( $altInfo ) { | |
$message = preg_replace( PrettyErrorsHandler::REGEX_METHOD_OR_FUNCTION_END, $altInfo, $message ); | |
list( $srcErrFile, $srcErrLine, $stackSearchI ) = $skipStackFirst( $stackTrace ); | |
} | |
} | |
} | |
/* | |
* Unexpected symbol errors. | |
* For example 'unexpected T_OBJECT_OPERATOR'. | |
* | |
* This swaps the 'T_WHATEVER' for the symbolic representation. | |
*/ | |
} else if ( $code === 4 ) { | |
$matches = array(); | |
$num = preg_match( '/\bunexpected ([A-Z_]+|\\$end)\b/', $message, $matches ); | |
if ( $num > 0 ) { | |
$match = $matches[0]; | |
$newSymbol = PrettyErrorsHandler::phpSymbolToDescription( str_replace('unexpected ', '', $match) ); | |
$message = str_replace( $match, "unexpected '$newSymbol'", $message ); | |
} | |
$matches = array(); | |
$num = preg_match( '/, expecting ([A-Z_]+|\\$end)\b/', $message, $matches ); | |
if ( $num > 0 ) { | |
$match = $matches[0]; | |
$newSymbol = PrettyErrorsHandler::phpSymbolToDescription( str_replace(', expecting ', '', $match) ); | |
$message = str_replace( $match, ", expecting '$newSymbol'", $message ); | |
} | |
/** | |
* Undefined Variable, add syntax highlighting and make variable from 'foo' too '$foo'. | |
*/ | |
} else if ( $code === 8 ) { | |
if ( | |
strpos($message, "Undefined variable") !== false | |
) { | |
$matches = array(); | |
preg_match( '/\b[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*$/', $message, $matches ); | |
if ( count($matches) > 0 ) { | |
$message = preg_replace( | |
'/\b[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*$/', | |
'<span class="syntax-variable">$' . $matches[0] . '</span>', | |
$message | |
); | |
} | |
} | |
/** | |
* Invalid type given. | |
*/ | |
} else if ( $code === 4096 ) { | |
if ( strpos($message, 'must be an ') ) { | |
$message = preg_replace( '/, called in .*$/', '', $message ); | |
$matches = array(); | |
preg_match( '/\b[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*(::[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*)?\\(\\)/', $message, $matches ); | |
if ( $matches ) { | |
$argumentMathces = array(); | |
preg_match( '/^Argument ([0-9]+)/', $message, $argumentMathces ); | |
$highlightArg = count($argumentMathces) === 2 ? | |
(((int) $argumentMathces[1])-1) : | |
null ; | |
$fun = PrettyErrorsHandler::syntaxHighlightFunctionMatch( $matches[0], $stackTrace, $highlightArg ); | |
if ( $fun ) { | |
$message = str_replace( 'passed to ', 'calling ', $message ); | |
$message = preg_replace( PrettyErrorsHandler::REGEX_METHOD_OR_FUNCTION, $fun, $message ); | |
$prioritizeCaller = true; | |
/* | |
* scalars not supported. | |
*/ | |
$scalarType = null; | |
if ( ! PrettyErrorsHandler::$IS_SCALAR_TYPE_HINTING_SUPPORTED ) { | |
foreach ( PrettyErrorsHandler::$SCALAR_TYPES as $scalar ) { | |
if ( stripos($message, "must be an instance of $scalar,") !== false ) { | |
$scalarType = $scalar; | |
break; | |
} | |
} | |
} | |
if ( $scalarType !== null ) { | |
$message = preg_replace( '/^Argument [0-9]+ /', 'Incorrect type hint ', $message ); | |
$message = preg_replace( | |
'/ must be an instance of ' . PrettyErrorsHandler::REGEX_PHP_IDENTIFIER . '\b.*$/', | |
", ${scalarType} not supported", | |
$message | |
); | |
$prioritizeCaller = false; | |
} else { | |
$message = preg_replace( '/ must be an (instance of )?' . PrettyErrorsHandler::REGEX_PHP_IDENTIFIER . '\b/', '', $message ); | |
if ( preg_match('/, none given$/', $message) ) { | |
$message = preg_replace( '/^Argument /', 'Missing argument ', $message ); | |
$message = preg_replace( '/, none given$/', '', $message ); | |
} else { | |
$message = preg_replace( '/^Argument /', 'Incorrect argument ', $message ); | |
} | |
} | |
if ( $prioritizeCaller ) { | |
list( $srcErrFile, $srcErrLine, $stackSearchI ) = $skipStackFirst( $stackTrace ); | |
} | |
} | |
} | |
} | |
} | |
if ( $stackTrace !== null ) { | |
if ( | |
count($stackTrace) > 0 && ( | |
(! isset($stackTrace[0]['line'])) || | |
($stackTrace[0]['line'] !== $errLine) | |
) | |
) { | |
array_unshift( $stackTrace, array( | |
'line' => $errLine, | |
'file' => $errFile | |
) ); | |
} | |
if ( $stackTrace ) { | |
$ignoreCommons = false; | |
$len = count($stackTrace); | |
/* | |
* The code above can prioritize a location in the stack trace, | |
* this is 'stackSearchI'. So we should start our search from there, | |
* and work down the stack. | |
* | |
* This is built in a way so that when it reaches the end, it'll loop | |
* back round to the beginning, and check the traces we didn't check | |
* last time. | |
* | |
* If stackSearchI was not altered, then it just searches from top | |
* through to the bottom. | |
*/ | |
for ( $i = $stackSearchI; $i < $stackSearchI+$len; $i++ ) { | |
$trace = &$stackTrace[ $i % $len ]; | |
if ( isset($trace['file']) && isset($trace['line']) ) { | |
$type = $this->getFolderType( $root, $trace['file'] ); | |
if ( $type !== PrettyErrorsHandler::FILE_TYPE_IGNORE ) { | |
if ( $type === PrettyErrorsHandler::FILE_TYPE_APPLICATION ) { | |
$srcErrLine = $trace['line']; | |
$srcErrFile = $trace['file']; | |
break; | |
} else if ( ! $ignoreCommons ) { | |
$srcErrLine = $trace['line']; | |
$srcErrFile = $trace['file']; | |
$ignoreCommons = true; | |
} | |
} | |
} | |
} | |
} | |
} | |
return array( $message, $srcErrFile, $srcErrLine, $altInfo ); | |
} | |
/** | |
* Parses the stack trace, and makes it look pretty. | |
* | |
* This includes adding in the syntax highlighting, | |
* highlighting the colours for the files, | |
* and padding with whitespace. | |
* | |
* If stackTrace is null, then null is returned. | |
*/ | |
private function parseStackTrace( $code, $message, $errLine, $errFile, &$stackTrace, $root, $altInfo=null ) { | |
if ( $stackTrace !== null ) { | |
/* | |
* For whitespace padding. | |
*/ | |
$lineLen = 0; | |
$functionLen = 0; | |
$fileLen = 0; | |
$identifyType = function( $arg, $recurse, $identifyType ) { | |
if ( ! is_array($arg) && !is_object($arg) ) { | |
if ( is_string($arg) ) { | |
return "<span class='syntax-string'>\"$arg\"</span>"; | |
} else { | |
return "<span class='syntax-literal'>" . var_export( $arg, true ) . '</span>'; | |
} | |
} else if ( is_array($arg) ) { | |
if ( count($arg) === 0 ) { | |
return "[]"; | |
} else if ( $recurse ) { | |
$argArr = array(); | |
foreach ($arg as $i => $ag) { | |
$argArr[]= $identifyType( $ag, false, $identifyType ); | |
} | |
return "[" . join(', ', $argArr) . "]"; | |
} else { | |
return "[...]"; | |
} | |
} else { | |
return '<span class="syntax-variable">$' . get_class( $arg ) . '</span>'; | |
} | |
}; | |
// parse the stack trace, and remove the long urls | |
foreach ( $stackTrace as $i => $trace ) { | |
if ( $trace ) { | |
$trace['info'] = ''; | |
if ( isset($trace['line'] ) ) { | |
$lineLen = max( $lineLen, strlen($trace['line']) ); | |
} else { | |
$trace['line'] = ''; | |
} | |
$info = ''; | |
if ( $i === 0 ) { | |
if ( $altInfo !== null ) { | |
$info = $altInfo; | |
} else if ( isset($trace['info']) && $trace['info'] !== '' ) { | |
$info = PrettyErrorsHandler::syntaxHighlight( $trace['info'] ); | |
} else { | |
$contents = $this->getFileContents( $trace['file'] ); | |
$info = PrettyErrorsHandler::syntaxHighlight( | |
trim( $contents[$trace['line']-1] ) | |
); | |
} | |
} else { | |
$args = array(); | |
if ( isset($trace['args']) ) { | |
foreach ( $trace['args'] as $arg ) { | |
$args[]= $identifyType( $arg, true, $identifyType ); | |
} | |
} | |
$info = PrettyErrorsHandler::syntaxHighlightFunction( | |
isset($trace['class']) ? $trace['class'] : null, | |
isset($trace['type']) ? $trace['type'] : null, | |
isset($trace['function']) ? $trace['function'] : null, | |
$args | |
); | |
} | |
$trace['info'] = $info; | |
if ( isset($trace['file']) ) { | |
$file = $this->removeRootPath( $root, $trace['file'] ); | |
$klass = ''; | |
if ( strpos($file, '/') === false ) { | |
$klass = 'file-root'; | |
} else { | |
$type = $this->getFolderType( $file ); | |
if ( $type === PrettyErrorsHandler::FILE_TYPE_IGNORE ) { | |
$klass = 'file-ignore'; | |
} else if ( $type === PrettyErrorsHandler::FILE_TYPE_APPLICATION ) { | |
$klass = 'file-app'; | |
} else { | |
$klass = 'file-common'; | |
} | |
} | |
$trace['html_file'] = "<span class='$klass'>$file</span>"; | |
} else { | |
$file = '[internal function]'; | |
$trace['html_file'] = "<span class='file-internal'>$file</span>"; | |
} | |
$trace['file'] = $file; | |
$fileLen = max( $fileLen, strlen($file) ); | |
$stackTrace[$i] = $trace; | |
} | |
} | |
/* | |
* We are allowed to highlight just once, that's it. | |
*/ | |
$highlightI = -1; | |
foreach ( $stackTrace as $i => $trace ) { | |
if ( | |
$trace['line'] === $errLine && | |
$trace['file'] === $errFile | |
) { | |
$highlightI = $i; | |
break; | |
} | |
} | |
foreach ( $stackTrace as $i => $trace ) { | |
if ( $trace ) { | |
$line = str_pad( $trace['line'] , $lineLen, ' ', STR_PAD_LEFT ); | |
$file = $trace['html_file'] . str_pad( '', $fileLen-strlen($trace['file']), ' ', STR_PAD_LEFT ); | |
$info = $trace['info']; | |
$info = str_replace( "\n", '\n', $info ); | |
$info = str_replace( "\r", '\r', $info ); | |
$stackStr = ( $info !== '' ) ? | |
"$line $file $info" : | |
"$line $file" ; | |
if ( $highlightI === $i ) { | |
$stackStr = "<div class='highlight'>$stackStr</div>"; | |
} else if ( $highlightI > $i ) { | |
$stackStr = "<div class='pre-highlight'>$stackStr</div>"; | |
} else { | |
$stackStr = "<div>$stackStr</div>"; | |
} | |
$stackTrace[$i] = $stackStr; | |
} | |
} | |
return join( "", $stackTrace ); | |
} else { | |
return null; | |
} | |
} | |
/** | |
* The entry point for handling an error. | |
*/ | |
public function reportError( $code, $message, $errLine, $errFile, $stackTrace=null, $ex=null ) { | |
$root = $this->documentRoot; | |
list( $message, $srcErrFile, $srcErrLine, $altInfo ) = | |
$this->improveErrorMessage( $ex, $code, $message, $errLine, $errFile, $root, $stackTrace ); | |
$fileLines = $this->readCodeFile( $srcErrFile, $srcErrLine ); | |
$errFile = $srcErrFile; | |
$errLine = $srcErrLine; | |
$errFile = $this->removeRootPath( $root, $errFile ); | |
$stackTrace = $this->parseStackTrace( $code, $message, $errLine, $errFile, $stackTrace, $root, $altInfo ); | |
$this->displayError( $message, $srcErrLine, $errFile, $stackTrace, $fileLines ); | |
} | |
/* | |
* Now the actual hooking into PHP's error reporting. | |
* | |
* We enable _ALL_ errors, and make them all exceptions. | |
* We also need to hook into the shutdown function so | |
* we can catch fatal and compile time errors. | |
*/ | |
private function attachErrorHandles() { | |
if ( $this->isOff() ) { | |
error_reporting( $this->defaultErrorReportingOff ); | |
} else { | |
$self = $this; | |
// all errors \o/ ! | |
error_reporting( $this->defaultErrorReportingOn ); | |
set_error_handler( | |
function( $code, $message, $file, $line, $context ) use ( $self ) { | |
/* | |
* DO NOT! log the error. | |
* | |
* Either it's thrown as an exception, and so logged by the exception handler, | |
* or we return false, and it's logged by PHP. | |
*/ | |
if ( $self->isOn() ) { | |
/* | |
* When using an @, the error reporting drops to 0. | |
*/ | |
if ( error_reporting() !== 0 || $this->catchSurpressedErrors ) { | |
throw new \pretty_errors\ErrorToExceptionException( $code, $message, $file, $line, $context ); | |
} | |
} else { | |
return false; | |
} | |
}, | |
$this->defaultErrorReportingOn | |
); | |
set_exception_handler( function($ex) use ( $self ) { | |
if ( $self->isOn() ) { | |
$file = $ex->getFile(); | |
$line = $ex->getLine(); | |
$message = $ex->getMessage(); | |
$trace = $ex->getTraceAsString(); | |
$parts = explode( "\n", $trace ); | |
$trace = " " . join( "\n ", $parts ); | |
error_log( "$message \n $file, $line \n$trace" ); | |
$self->reportError( $ex->getCode(), $message, $line, $file, $ex->getTrace(), $ex ); | |
} else { | |
return false; | |
} | |
}); | |
if ( ! $self->isShutdownRegistered ) { | |
if ( $self->catchClassNotFound ) { | |
spl_autoload_register( function($className) use ( $self ) { | |
if ( $self->isOn() ) { | |
throw new \pretty_errors\ErrorToExceptionException( E_ERROR, "Class '$className' not found", __FILE__, __LINE__ ); | |
} | |
} ); | |
} | |
register_shutdown_function( function() use ( $self ) { | |
if ( $self->isOn() ) { | |
$error = error_get_last(); | |
// fatal and syntax errors | |
if ( | |
$error && ( | |
$error['type'] === 1 || | |
$error['type'] === 4 | |
) | |
) { | |
$self->reportError( $error['type'], $error['message'], $error['line'], $error['file'] ); | |
} | |
} | |
}); | |
$self->isShutdownRegistered = true; | |
} | |
} | |
} | |
/** | |
* The actual display logic. | |
* This outputs the error details in HTML. | |
*/ | |
private function displayError( $message, $errLine, $errFile, $stackTrace, &$fileLines ) { | |
$documentRoot = $this->documentRoot; | |
$serverName = $this->serverName; | |
$backgroundText = $this->backgroundText; | |
\pretty_errors\displayHTML( | |
// pre, in the head | |
function() use( $message, $errFile, $errLine ) { | |
echo "<!--\n" . | |
"$message\n" . | |
"$errFile, $errLine\n" . | |
"-->"; | |
}, | |
// the content | |
function() use ( | |
$backgroundText, $serverName, $documentRoot, | |
$message, $errLine, $errFile, $stackTrace, &$fileLines | |
) { | |
?><div id="error-wrap"> | |
<div id="error-back"><?= $backgroundText ?></div> | |
</div> | |
<h2 id="error-file-root"><?= $serverName ?> | <?= $documentRoot ?></h2> | |
<h1 id="error-title"><?= $message ?></h1> | |
<h2 id="error-file" class="<?= $fileLines ? 'has_code' : '' ?>"><?= $errFile ?>, <?= $errLine ?></h2> | |
<? if ( $fileLines ) { ?> | |
<ul id="error-file-lines"> | |
<? foreach ( $fileLines as $lineNum => $line ) { ?> | |
<li class="error-file-line <?= ($lineNum === $errLine) ? 'highlight' : '' ?>"><?= $line ?></li> | |
<? } ?> | |
</ul> | |
<? } ?> | |
<? if ( $stackTrace !== null ) { ?> | |
<div id="error-stack-trace"><?= $stackTrace ?></div> | |
<? } | |
} | |
); | |
} | |
} | |
/** | |
* A generic function for clearing the buffer, and displaying error output. | |
*/ | |
function displayHTML( $pre, $content=null ) { | |
if ( func_num_args() === 1 ) { | |
$content = $pre; | |
$pre = null; | |
} | |
// clean out anything displayed already | |
try { | |
ob_end_clean(); | |
ob_start("ob_gzhandler"); | |
} catch ( \Exception $ex ) { /* do nothing */ } | |
echo '<!DOCTYPE html>'; | |
if ( $pre !== null ) { | |
$pre(); | |
} | |
?><style> | |
html, body { margin: 0; padding: 0; } | |
body { | |
width: 100%; | |
height: 100%; | |
padding: 16px 32px; | |
-moz-box-sizing: border-box; | |
box-sizing: border-box; | |
background: #111; | |
color: #f0f0f0; | |
} | |
::-moz-selection{background: #662039 !important; color: #fff !important; text-shadow: none;} | |
::selection {background: #662039 !important; color: #fff !important; text-shadow: none;} | |
a, | |
a:visited, | |
a:hover, | |
a:active { | |
color: #9ae; | |
text-decoration: undefine; | |
} | |
a:hover { | |
color: #aff; | |
} | |
h1 { | |
font: 32px consolas; | |
margin-bottom: 0; | |
} | |
h2 { | |
font: 24px consolas; | |
margin-top: 0; | |
} | |
#error-stack-trace { | |
font: 18px consolas; | |
line-height: 28px; | |
white-space: pre; | |
cursor: pointer; | |
} | |
#error-file.has_code { | |
margin: 36px 0 -6px 128px; | |
} | |
#error-file-root { | |
color: #555; | |
} | |
#error-file-lines { | |
margin-left : 128px; | |
margin-bottom: 38px; | |
padding: 0 18px 9px 0; | |
display: inline-block; | |
} | |
.error-file-line { | |
font: 16px consolas; | |
color: #ddd; | |
white-space: pre; | |
list-style-type: none; | |
/* needed for empty lines */ | |
min-height: 20px; | |
} | |
.pre-highlight, | |
.highlight { | |
width: 100%; | |
color: #eee; | |
} | |
.pre-highlight { | |
opacity: 0.3; | |
color: #999 !important; | |
} | |
.pre-highlight span { | |
color: #999 !important; | |
border: none !important; | |
} | |
.highlight { | |
background: #391818; | |
box-shadow: 0 0 6px #301010; | |
border-radius: 2px; | |
padding-bottom: 1px; | |
} | |
.syntax-class { | |
color: #C07041; | |
} | |
.syntax-string { | |
color: #7C9D5D; | |
} | |
.syntax-literal { | |
color: #cF5d33; | |
} | |
.syntax-variable-not-important { | |
opacity: 0.5; | |
} | |
.syntax-higlight-variable { | |
color: #f00; | |
border-bottom: 3px dashed #c33; | |
} | |
.syntax-variable { | |
color: #798aA0; | |
} | |
.syntax-keyword { | |
color: #C07041; | |
} | |
.syntax-function { | |
color: #F9EE98; | |
} | |
.syntax-comment { | |
color: #5a5a5a; | |
} | |
.file-internal { | |
color: #555; | |
} | |
.file-common { | |
color: #eb4; | |
} | |
.file-ignore { | |
color: #585; | |
} | |
.file-app { | |
color: #7bc; | |
} | |
.file-root { | |
color: #b69; | |
} | |
#error-wrap { | |
right: 0; | |
top: 0; | |
position: absolute; | |
overflow: hidden; | |
z-index: -1; | |
width: 100%; | |
height: 100%; | |
} | |
#error-back { | |
font: 240px consolas; | |
color: #211600; | |
position: absolute; | |
top: 60px; | |
right: -80px; | |
-webkit-transform: rotate( 24deg ); | |
-moz-transform: rotate( 24deg ); | |
-ms-transform: rotate( 24deg ); | |
-o-transform: rotate( 24deg ); | |
transform: rotate( 24deg ); | |
} | |
</style><? | |
$content(); | |
} | |
/** | |
* This is a wrapper class, for wrapping errors inside | |
* of Exceptions. This way errors can now be terminal, | |
* and provide a stack trace. | |
*/ | |
class ErrorToExceptionException extends Exception | |
{ | |
protected $_context = null; | |
public function __construct( $code, $message, $file, $line, $context=null ) | |
{ | |
parent::__construct( $message, $code ); | |
$this->file = $file; | |
$this->line = $line; | |
$this->setContext( $context ); | |
} | |
public function getContext() | |
{ | |
return $this->_context; | |
} | |
public function setContext( $value ) | |
{ | |
$this->_context = $value; | |
} | |
} | |
class CallErrorToExceptionException extends ErrorToExceptionException | |
{ | |
private $fun; | |
private $args; | |
public function __construct( $code, $message, $file, $line, $fun, $args ) { | |
parent::__construct( $code, $message, $file, $line ); | |
$this->fun = $fun; | |
$this->args = $args; | |
} | |
public function getFunctionName() { | |
return $this->fun; | |
} | |
public function getArgs() { | |
return $this->args; | |
} | |
public function getNumArgs() { | |
return count( $this->args ); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment