Created
June 7, 2012 21:29
-
-
Save JosephLenton/2891648 to your computer and use it in GitHub Desktop.
pretty errors
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; | |
global $_pretty_errors_global_handler; | |
$_pretty_errors_global_handler = null; | |
/** | |
* | |
*/ | |
function updateFile( $name, $content ) | |
{ | |
file_put_contents( $name, $content ); | |
} | |
function reportErrors( $options=null ) { | |
$handler = new \pretty_errors\PrettyErrorsHandler( $options ); | |
return $handler->turnOn(); | |
} | |
class PrettyErrorsHandler | |
{ | |
/** | |
* 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; | |
/** | |
* 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( | |
'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(\pretty_errors\PrettyErrorsHandler::$PHP_SYMBOL_MAPPINGS[$symbol]) ) { | |
return \pretty_errors\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 = \pretty_errors\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 ); | |
} | |
/** | |
* 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 ) { | |
$info = ''; | |
// 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 ( count($args) > 0 ) { | |
$info = $info . '( ' . join( ', ', $args ) . ' )'; | |
} else { | |
$info = $info . '()'; | |
} | |
return $info; | |
} | |
private $cachedFiles; | |
private $isShutdownRegistered; | |
private $isOn; | |
private $ignoreFolders = array(); | |
private $ignoreFoldersLongest = 0; | |
/** | |
* Options include: | |
* - ignore_folder | |
*/ | |
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. | |
*/ | |
if ( $options !== null ) { | |
if ( isset($options['ignore_folders']) ) { | |
$this->setIgnoreFolders( $options['ignore_folders'] ); | |
} | |
} | |
} | |
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 isIgnoreFolder( $root, $file ) { | |
$file = $this->removeRootPath( $root, $file ); | |
$parts = explode( '/', $file ); | |
$len = min( count($parts), $this->ignoreFoldersLongest ); | |
for ( $i = 0; $i < $len; $i++ ) { | |
if ( isset($this->ignoreFolders[$i+1]) ) { | |
$ignores = &$this->ignoreFolders[$i+1]; | |
$success = false; | |
for ( $j = 0; $j < count($ignores); $j++ ) { | |
if ( $ignores[$j] === $parts[$j] ) { | |
$success = true; | |
} else { | |
$success = false; | |
break; | |
} | |
} | |
if ( $success ) { | |
return true; | |
} | |
} | |
} | |
return false; | |
} | |
private function setIgnoreFolders( $folders ) { | |
$ignoreFolders = array(); | |
$longestIgnore = 0; | |
foreach ( $folders as $i => $folder ) { | |
$folder = str_replace( '\\', '/', $folder ); | |
$folder = preg_replace( '/(^\\/+)|(\\/+$)/', '', $folder ); | |
$parts = explode( '/', $folder ); | |
$count = count( $parts ); | |
$longestIgnore = max( $longestIgnore, $count ); | |
if ( isset($ignoreFolders[$count]) ) { | |
$folds = &$ignoreFolders[$count]; | |
$folds[]= $folder; | |
} else { | |
$ignoreFolders[$count] = array( $folder ); | |
} | |
} | |
$this->ignoreFolders = $ignoreFolders; | |
$this->ignoreFoldersLongest = $longestIgnore; | |
} | |
/** | |
* 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 = \pretty_errors\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 = \pretty_errors\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( $code, $message, $errLine, $errFile, $root, &$stackTrace ) { | |
// change these to change where the source file is come from | |
$srcErrFile = $errFile; | |
$srcErrLine = $errLine; | |
$functionSignature = null; | |
/* | |
* This is for missing argument errors. | |
* | |
* 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 ( count($matches) > 0 ) { | |
$match = $matches[0]; | |
list( $className, $type, $functionName ) = \pretty_errors\PrettyErrorsHandler::splitFunction( $match ); | |
$args = array(); | |
$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]*)?\\(\\)$/', | |
\pretty_errors\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( '/\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 ( count($matches) > 0 ) { | |
list( $className, $type, $functionName ) = \pretty_errors\PrettyErrorsHandler::splitFunction( $matches[0] ); | |
// is class::method() | |
if ( $className !== null ) { | |
$reflectClass = new \ReflectionClass( $className ); | |
$params = $reflectClass->getMethod( $functionName )->getParameters(); | |
// is a function | |
} else { | |
$reflectFun = new \ReflectionFunction( $functionName ); | |
$params = $reflectFun->getParameters(); | |
} | |
if ( $params !== null && count($params) > 0 ) { | |
$args = array(); | |
for ( $i = 0; $i < count($params); $i++ ) { | |
$param = $params[$i]; | |
$paramsStr = ''; | |
if ( $param->isPassedByReference() ) { | |
$paramsStr = '<span class="syntax-function">&</span>'; | |
} | |
$paramsStr .= '<span class="syntax-variable">'; | |
$paramsStr .= '$'.$param->getName() . '</span>'; | |
if ( $param->isDefaultValueAvailable() ) { | |
$paramsStr .= '=<span class="syntax-literal">' . var_export( $param->getDefaultValue(), true ) . '</span>'; | |
} | |
$args[]= $paramsStr; | |
} | |
if ( $className !== null && $stackTrace && isset($stackTrace[1]) && isset($stackTrace[1]['type']) ) { | |
$type = htmlspecialchars( $stackTrace[1]['type'] ); | |
} else { | |
$type = null; | |
} | |
$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]*)?\\(\\)$/', | |
\pretty_errors\PrettyErrorsHandler::syntaxHighlightFunction( $className, $type, $functionName, $args ), | |
$message | |
); | |
$functionSignature = array( | |
'class' => $className, | |
'type' => $type, | |
'function' => $functionName, | |
'args' => $args | |
); | |
} | |
} | |
if ( $stackTrace && isset($stackTrace[1]) && isset($stackTrace[1]['type']) ) { | |
if ( $stackTrace[1]['type'] === '->' ) { | |
$message = str_replace( '::', '->', $message ); | |
} | |
} | |
$skipFirst = true; | |
foreach ( $stackTrace as $trace ) { | |
if ( $skipFirst ) { | |
$skipFirst = false; | |
} else { | |
if ( $trace && isset($trace['file']) && isset($trace['line']) ) { | |
$srcErrFile = $trace['file']; | |
$srcErrLine = $trace['line']; | |
break; | |
} | |
} | |
} | |
} | |
/* | |
* 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 = \pretty_errors\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 = \pretty_errors\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 | |
); | |
} | |
} | |
} | |
if ( $stackTrace !== null ) { | |
if ( | |
count($stackTrace) > 0 && ( | |
(! isset($stackTrace[0]['line'])) || | |
($stackTrace[0]['line'] !== $errLine) | |
) | |
) { | |
array_unshift( $stackTrace, array( | |
'line' => $errLine, | |
'file' => $errFile | |
) ); | |
} | |
if ( count($stackTrace) > 0 ) { | |
foreach ( $stackTrace as $i => &$trace ) { | |
if ( isset($trace['file']) && isset($trace['line']) ) { | |
if ( $this->isIgnoreFolder( $root, $trace['file'] ) ) { | |
$keepNext = true; | |
} else { | |
if ( | |
$srcErrLine !== $trace['line'] || | |
$srcErrFile !== $trace['file'] | |
) { | |
$srcErrLine = $trace['line']; | |
$srcErrFile = $trace['file']; | |
} | |
break; | |
} | |
} | |
} | |
} | |
} | |
return array( $message, $srcErrFile, $srcErrLine, $functionSignature ); | |
} | |
/** | |
* 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, $functionSignature ) { | |
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 ( $functionSignature !== null ) { | |
$info = \pretty_errors\PrettyErrorsHandler::syntaxHighlightFunction( | |
$functionSignature['class'], | |
$functionSignature['type'], | |
$functionSignature['function'], | |
$functionSignature['args'] | |
); | |
} else if ( isset($trace['info']) && $trace['info'] !== '' ) { | |
$info = \pretty_errors\PrettyErrorsHandler::syntaxHighlight( $trace['info'] ); | |
} else { | |
$contents = $this->getFileContents( $trace['file'] ); | |
$info = \pretty_errors\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 = \pretty_errors\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'] ); | |
if ( strpos($file, '\\') === false && strpos($file, '/') === false ) { | |
$trace['html_file'] = "<span class='file-root'>$file</span>"; | |
} else if ( strpos($file, 'flexi/') === 0 || strpos($file, 'flexi\\') === 0 ) { | |
$trace['html_file'] = "<span class='file-flexi'>$file</span>"; | |
} else if ( strpos($file, 'app/') === 0 || strpos($file, 'app\\') === 0 ) { | |
$trace['html_file'] = "<span class='file-app'>$file</span>"; | |
} else { | |
$trace['html_file'] = "<span class='file-common'>$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 ) { | |
$root = str_replace('\\', '/', $_SERVER['DOCUMENT_ROOT']); | |
list( $message, $srcErrFile, $srcErrLine, $functionSignature ) = | |
$this->improveErrorMessage( $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, $functionSignature ); | |
$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( E_ALL & ~E_DEPRECATED ); | |
} else { | |
$self = $this; | |
// all errors \o/ ! | |
error_reporting( E_ALL | E_DEPRECATED ); | |
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() ) { | |
throw new \pretty_errors\ErrorToExceptionException( $code, $message, $file, $line, $context ); | |
} else { | |
return false; | |
} | |
} ); | |
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() ); | |
} else { | |
return false; | |
} | |
}); | |
if ( ! $self->isShutdownRegistered ) { | |
spl_autoload_register( function($className) { | |
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 ) { | |
\pretty_errors\displayHTML( | |
// pre, in the head | |
function() use( $message, $errFile, $errLine ) { | |
echo "<!--\n" . | |
"$message\n" . | |
"$errFile, $errLine\n" . | |
"-->"; | |
}, | |
// the content | |
function() use ( $message, $errLine, $errFile, $stackTrace, &$fileLines ) { | |
?><div id="error-wrap"> | |
<div id="error-back"></div> | |
</div> | |
<h2 id="error-file-root"><?= $_SERVER['SERVER_NAME'] ?> | <?= $_SERVER['DOCUMENT_ROOT'] ?></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; | |
} | |
#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; | |
} | |
.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 { | |
color: #798aA0; | |
} | |
.syntax-keyword { | |
color: #C07041; | |
} | |
.syntax-function { | |
color: #F9EE98; | |
} | |
.file-internal { | |
color: #555; | |
} | |
.file-common { | |
color: #9aa; | |
} | |
.file-flexi { | |
color: #585; | |
} | |
.file-app { | |
color: #6ac; | |
} | |
.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: #241212; | |
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; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment