Skip to content

Instantly share code, notes, and snippets.

@JosephLenton
Created June 7, 2012 21:29
Show Gist options
  • Save JosephLenton/2891648 to your computer and use it in GitHub Desktop.
Save JosephLenton/2891648 to your computer and use it in GitHub Desktop.
pretty errors
<?
/**
* 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 = '&amp;';
$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 = '-&gt;';
}
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 = '-&gt;';
}
$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">&amp;</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