Created
July 7, 2011 16:58
-
-
Save nicolas-grekas/1069975 to your computer and use it in GitHub Desktop.
Advanced error handling in PHP
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
<?php /****************** vi: set fenc=utf-8 ts=4 sw=4 et: ***************** | |
* | |
* Copyright : (C) 2011 Nicolas Grekas. All rights reserved. | |
* Email : [email protected] | |
* License : http://www.gnu.org/licenses/lgpl.txt GNU/LGPL | |
* | |
* This program is free software; you can redistribute it and/or modify | |
* it under the terms of the GNU Lesser General Public License as | |
* published by the Free Software Foundation, either version 3 of the | |
* License, or (at your option) any later version. | |
* | |
***************************************************************************/ | |
namespace Patchwork\PHP; | |
/** | |
* Dumper extends Walker and adds managing depth and length limits, alongside with | |
* a callback mechanism for getting detailed information about objects and resources. | |
* | |
* For example and by default, resources of type stream are expanded by stream_get_meta_data, | |
* those of type process by proc_get_status, and closures are associated with a method that | |
* uses reflection to provide detailed information about anonymous functions. This class is | |
* designed to implement these mechanisms in a way independent of the final representation. | |
*/ | |
abstract class Dumper extends Walker | |
{ | |
public | |
$maxLength = 100, | |
$maxDepth = 10; | |
protected | |
$depthLimited = array(), | |
$reserved = array('_' => 1, '__cutBy' => 1, '__refs' => 1, '__proto__' => 1), | |
$callbacks = array( | |
'o:closure' => array(__CLASS__, 'castClosure'), | |
'r:stream' => 'stream_get_meta_data', | |
'r:process' => 'proc_get_status', | |
); | |
function setCallback($type, $callback) | |
{ | |
$this->callbacks[strtolower($type)] = $callback; | |
} | |
protected function dumpObject($obj) | |
{ | |
$c = get_class($obj); | |
$p = array($c => $c) | |
+ class_parents($obj) | |
+ class_implements($obj) | |
+ array('*' => '*'); | |
foreach ($p as $p) | |
{ | |
if (isset($this->callbacks[$p = 'o:' . strtolower($p)])) | |
{ | |
if (!$p = $this->callbacks[$p]) $a = array(); | |
else | |
{ | |
try {$a = call_user_func($p, $obj);} | |
catch (\Exception $e) {unset($a); continue;} | |
} | |
break; | |
} | |
} | |
isset($a) || $a = (array) $obj; | |
$this->walkHash($c, $a); | |
} | |
protected function dumpResource($res) | |
{ | |
$h = get_resource_type($res); | |
if (empty($this->callbacks['r:' . $h])) $res = array(); | |
else | |
{ | |
try {$res = call_user_func($this->callbacks['r:' . $h], $res);} | |
catch (\Exception $e) {$res = array();} | |
} | |
$this->walkHash("resource:{$h}", $res); | |
} | |
protected function dumpRef($is_soft, $ref_counter = null, &$ref_value = null) | |
{ | |
if (null !== $ref_value && isset($this->depthLimited[$ref_counter]) && $this->depth !== $this->maxDepth) | |
{ | |
unset($this->depthLimited[$ref_counter]); | |
switch (true) | |
{ | |
case is_resource($ref_value): $this->dumpResource($ref_value); return true; | |
case is_object($ref_value): $this->dumpObject($ref_value); return true; | |
case is_array($ref_value): | |
$ref_counter = count($ref_value); | |
isset($ref_value[$this->token]) && --$ref_counter; | |
$this->walkHash('array:' . $ref_counter, $ref_value); | |
return true; | |
} | |
} | |
return false; | |
} | |
protected function walkHash($type, &$a) | |
{ | |
$len = count($a); | |
isset($a[$this->token]) && --$len; | |
if ($len && $this->depth === $this->maxDepth && 0 < $this->maxDepth) | |
{ | |
$this->depthLimited[$this->counter] = 1; | |
if (isset($this->refPool[$this->counter])) | |
$this->refPool[$this->counter]['ref_counter'] = $this->counter; | |
$this->dumpString('__cutBy', true); | |
$this->dumpScalar($len); | |
$len = 0; | |
} | |
if (!$len) return array(); | |
$i = 0; | |
++$this->depth; | |
if (false !== strpos($type, ':')) unset($type); | |
foreach ($a as $k => &$a) | |
{ | |
if ($k === $this->token) continue; | |
else if ($i === $this->maxLength && 0 < $this->maxLength) | |
{ | |
if ($len -= $i) | |
{ | |
$this->dumpString('__cutBy', true); | |
$this->dumpScalar($len); | |
} | |
break; | |
} | |
else if (isset($type, $k[0]) && "\0" === $k[0]) $k = implode(':', explode("\0", substr($k, 1), 2)); | |
else if (isset($this->reserved[$k]) || false !== strpos($k, ':')) $k = ':' . $k; | |
$this->dumpString($k, true); | |
$this->walkRef($a); | |
++$i; | |
} | |
if (--$this->depth) return array(); | |
else return $this->cleanRefPools(); | |
} | |
static function castClosure($c) | |
{ | |
$a = array(); | |
if (!class_exists('ReflectionFunction', false)) return $a; | |
$c = new \ReflectionFunction($c); | |
$c->returnsReference() && $a[] = '&'; | |
foreach ($c->getParameters() as $p) | |
{ | |
$n = ($p->isPassedByReference() ? '&$' : '$') . $p->getName(); | |
if ($p->isDefaultValueAvailable()) $a[$n] = $p->getDefaultValue(); | |
else $a[] = $n; | |
} | |
$a['use'] = array(); | |
if (false === $a['file'] = $c->getFileName()) unset($a['file']); | |
else $a['lines'] = $c->getStartLine() . '-' . $c->getEndLine(); | |
if (!$c = $c->getStaticVariables()) unset($a['use']); | |
else foreach ($c as $p => &$c) $a['use']['$' . $p] =& $c; | |
return $a; | |
} | |
} |
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
<?php /****************** vi: set fenc=utf-8 ts=4 sw=4 et: ***************** | |
* | |
* Copyright : (C) 2011 Nicolas Grekas. All rights reserved. | |
* Email : [email protected] | |
* License : http://www.gnu.org/licenses/lgpl.txt GNU/LGPL | |
* | |
* This program is free software; you can redistribute it and/or modify | |
* it under the terms of the GNU Lesser General Public License as | |
* published by the Free Software Foundation, either version 3 of the | |
* License, or (at your option) any later version. | |
* | |
***************************************************************************/ | |
namespace Patchwork\PHP; | |
/** | |
* ErrorHandler is a tunable error and exception handler. | |
* | |
* It provides four bit fields that control how errors are handled: | |
* - scream: never silenced errors | |
* - recoverableErrors: errors not logged but throwing a RecoverableErrorException | |
* - scopedErrors: errors logged with their local scope | |
* - tracedErrors: errors logged with their trace, but only once for repeated errors | |
* | |
* Errors are logged with a Logger object by default, but any logger can be injected | |
* provided it has the right interface. Errors are logged to the same file where non | |
* catchable errors are written by PHP. Silenced non catchable errors that can be | |
* detected at shutdown time are logged when the scream bit field allows so. | |
* | |
* Uncaught exceptions are turned to E_ERROR. | |
* | |
* As errors have a performance cost, repeated errors are all logged, so that the developper | |
* can see them and weight them as more important to fix than others of the same level. | |
*/ | |
class ErrorHandler | |
{ | |
public | |
$scream = 0x51, // E_ERROR | E_CORE_ERROR | E_COMPILE_ERROR | |
$recoverableErrors = 0x1100, // E_RECOVERABLE_ERROR | E_USER_ERROR | |
$scopedErrors = 0x0203, // E_ERROR | E_WARNING | E_USER_WARNING | |
$tracedErrors = 0x1302; // E_RECOVERABLE_ERROR | E_USER_ERROR | E_WARNING | E_USER_WARNING | |
protected | |
$logger, | |
$loggedTraces = array(), | |
$registeredErrors = 0; | |
protected static | |
$logFile, | |
$logStream, | |
$shuttingDown = 0, | |
$handlers = array(); | |
static function start($log_file = 'php://stderr', self $handler = null) | |
{ | |
null === $handler && $handler = new self; | |
// See also http://php.net/error_reporting | |
// Formatting errors with html_errors, error_prepend_string or | |
// error_append_string only works with displayed errors, not logged ones. | |
ini_set('display_errors', false); | |
ini_set('log_errors', true); | |
ini_set('error_log', $log_file); | |
// Some fatal errors can be caught at shutdown time! | |
// Then, any fatal error is really fatal: remaining shutdown | |
// functions, output buffering handlers or destructors are not called! | |
register_shutdown_function(array(__CLASS__, 'shutdown')); | |
self::$logFile = $log_file; | |
// Register the handler and top it to the current error_reporting() level | |
$handler->register(error_reporting()); | |
return $handler; | |
} | |
static function getHandler() | |
{ | |
return end(self::$handlers); | |
} | |
static function shutdown() | |
{ | |
self::$shuttingDown = 1; | |
if (false === $handler = end(self::$handlers)) return; | |
if ($e = self::getLastError()) | |
{ | |
switch ($e['type']) | |
{ | |
// Get the last uncatchable error | |
case E_ERROR: case E_PARSE: | |
case E_CORE_ERROR: case E_CORE_WARNING: | |
case E_COMPILE_ERROR: case E_COMPILE_WARNING: | |
$handler->handleLastError($e); | |
self::resetLastError(); | |
} | |
} | |
} | |
static function getLastError() | |
{ | |
$e = error_get_last(); | |
return empty($e['message']) ? false : $e; | |
} | |
static function resetLastError() | |
{ | |
// Reset error_get_last() by triggering a silenced empty user notice | |
set_error_handler(array(__CLASS__, 'falseError')); | |
$r = error_reporting(81); | |
user_error('', E_USER_NOTICE); | |
error_reporting($r); | |
restore_error_handler(); | |
} | |
static function falseError() | |
{ | |
return false; | |
} | |
function register($error_types = -1) | |
{ | |
$this->registeredErrors = $error_types; | |
set_exception_handler(array($this, 'handleException')); | |
set_error_handler(array($this, 'handleError'), $error_types); | |
self::$handlers[] = $this; | |
} | |
function unregister() | |
{ | |
$ok = array( | |
$this === end(self::$handlers), | |
array($this, 'handleError') === set_error_handler(array(__CLASS__, 'falseError')), | |
array($this, 'handleException') === set_exception_handler(array(__CLASS__, 'falseError')), | |
); | |
if ($ok = array(true, true, true) === $ok) | |
{ | |
array_pop(self::$handlers); | |
restore_error_handler(); | |
restore_exception_handler(); | |
$this->registeredErrors = 0; | |
} | |
else user_error('Failed to unregister: the current error or exception handler is not me', E_USER_WARNING); | |
restore_error_handler(); | |
restore_exception_handler(); | |
return $ok; | |
} | |
function handleError($type, $message, $file, $line, $scope, $trace_offset = 0, $log_time = 0) | |
{ | |
$throw = $this->recoverableErrors & $type; | |
$log = error_reporting() & $type; | |
if ($log || $throw || $scream = $this->scream & $type) | |
{ | |
$log_time || $log_time = microtime(true); | |
if ($throw) | |
{ | |
// To prevent extra logging of caught RecoverableErrorException and | |
// to remove logged and uncaught exception messages duplication and | |
// to dismiss any cryptic "Exception thrown without a stack frame" | |
// recoverable errors are logged but only at shutdown time. | |
$throw = new RecoverableErrorException($message, 0, $type, $file, $line); | |
$scream = self::$shuttingDown ? 1 : $log = 0; | |
} | |
if (0 <= $trace_offset) | |
{ | |
++$trace_offset; | |
// For duplicate errors, log the trace only once | |
$e = md5("{$type}/{$line}/{$file}\x00{$message}", true); | |
if (!($this->tracedErrors & $type) || isset($this->loggedTraces[$e])) $trace_offset = -1; | |
else if ($log) $this->loggedTraces[$e] = 1; | |
} | |
if ($log || $scream) | |
{ | |
$e = compact('type', 'message', 'file', 'line'); | |
$e['level'] = $type . '/' . error_reporting(); | |
$line = 0; // Read $trace_args | |
if ($log) | |
{ | |
if ($this->scopedErrors & $type) | |
{ | |
null !== $scope && $e['scope'] = $scope; | |
0 <= $trace_offset && $e['trace'] = debug_backtrace(true); // DEBUG_BACKTRACE_PROVIDE_OBJECT | |
$line = 1; | |
} | |
else if ($throw && 0 <= $trace_offset) $e['trace'] = $throw->getTrace(); | |
else if (0 <= $trace_offset) $e['trace'] = debug_backtrace(/*<*/PHP_VERSION_ID >= 50306 ? DEBUG_BACKTRACE_IGNORE_ARGS : false/*>*/); | |
} | |
$this->getLogger()->logError($e, $trace_offset, $line, $log_time); | |
} | |
if ($throw) | |
{ | |
$throw->scope = $scope; | |
$log || $throw->traceOffset = $trace_offset; | |
throw $throw; | |
} | |
} | |
return (bool) $log; | |
} | |
function handleException(\Exception $e, $log_time = 0) | |
{ | |
$this->recoverableErrors &= ~E_ERROR; // Prevent any accidental rethrow | |
$this->handleError( | |
E_ERROR, "Uncaught exception '" . get_class($e) . "'", | |
$e->getFile(), $e->getLine(), | |
array('uncaught-exception' => $e), | |
-1, $log_time | |
); | |
} | |
function handleLastError($e) | |
{ | |
// Handle errors when they have not been logged by the native PHP error handler. | |
// If this is the first event, handle it also to log any context data with it. | |
// Otherwise, do not duplicate it. | |
if (isset($this->logger) && (error_reporting() & $e['type'])) return; | |
call_user_func_array(array($this, 'handleError'), $e + array(null, -1)); | |
} | |
function getLogger() | |
{ | |
if (isset($this->logger)) return $this->logger; | |
isset(self::$logStream) || self::$logStream = fopen(self::$logFile, 'ab'); | |
return $this->logger = new Logger(self::$logStream); | |
} | |
} | |
class RecoverableErrorException extends \ErrorException implements RecoverableErrorInterface | |
{ | |
public $traceOffset = -1, $scope = array(); | |
} | |
interface RecoverableErrorInterface {} |
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
<?php | |
use Patchwork\PHP as p; | |
header('Content-type: text/plain'); | |
error_reporting(E_ALL | E_STRICT); | |
ini_set('display_errors', true); | |
include 'Walker.php'; | |
include 'Dumper.php'; | |
include 'JsonDumper.php'; | |
include 'Logger.php'; | |
include 'ErrorHandler.php'; | |
p\ErrorHandler::start('php://stderr')->getLogger()->log( | |
'debug-start', | |
array( | |
'start-time' => date('c'), | |
'request-context' => $_SERVER, | |
) | |
); | |
register_shutdown_function('log_shutdown'); | |
user_error('user triggered warning', E_USER_WARNING); | |
echo $a; // undefined variable | |
eval('a()'); // non-fatal parse error | |
@eval('a();'); // silenced undefined function fatal error | |
function log_shutdown() | |
{ | |
p\DebugLog::getHandler()->getLogger()->log('debug-shutdown', array( | |
'response-headers' => headers_list(), | |
)); | |
} |
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
<?php /****************** vi: set fenc=utf-8 ts=4 sw=4 et: ***************** | |
* | |
* Copyright : (C) 2011 Nicolas Grekas. All rights reserved. | |
* Email : [email protected] | |
* License : http://www.gnu.org/licenses/lgpl.txt GNU/LGPL | |
* | |
* This program is free software; you can redistribute it and/or modify | |
* it under the terms of the GNU Lesser General Public License as | |
* published by the Free Software Foundation, either version 3 of the | |
* License, or (at your option) any later version. | |
* | |
***************************************************************************/ | |
namespace Patchwork\PHP; | |
/** | |
* JsonDumper implements the JSON convention to dump any PHP variable with high accuracy. | |
* | |
* See https://github.com/nicolas-grekas/Patchwork-Doc/blob/master/Dumping-PHP-Data-en.md | |
*/ | |
class JsonDumper extends Dumper | |
{ | |
public | |
$maxString = 100000; | |
protected | |
$line = '', | |
$lines = array(), | |
$lastHash = 0; | |
static function dump(&$a) | |
{ | |
$d = new self; | |
$d->setCallback('line', array($d, 'echoLine')); | |
$d->walk($a); | |
} | |
static function get($a) | |
{ | |
$d = new self; | |
$d->setCallback('line', array($d, 'stackLine')); | |
$d->walk($a); | |
return implode("\n", $d->lines); | |
} | |
function walk(&$a) | |
{ | |
$this->line = ''; | |
parent::walk($a); | |
'' !== $this->line && $this->dumpLine(0); | |
} | |
protected function dumpLine($depth_offset) | |
{ | |
call_user_func($this->callbacks['line'], $this->line, $this->depth + $depth_offset); | |
$this->line = ''; | |
} | |
protected function dumpRef($is_soft, $ref_counter = null, &$ref_value = null) | |
{ | |
if (parent::dumpRef($is_soft, $ref_counter, $ref_value)) return; | |
$is_soft = $is_soft ? 'r' : 'R'; | |
$this->line .= "\"{$is_soft}`{$this->counter}:{$ref_counter}\""; | |
} | |
protected function dumpScalar($a) | |
{ | |
switch (true) | |
{ | |
case null === $a: $this->line .= 'null'; break; | |
case true === $a: $this->line .= 'true'; break; | |
case false === $a: $this->line .= 'false'; break; | |
case INF === $a: $this->line .= '"n`INF"'; break; | |
case -INF === $a: $this->line .= '"n`-INF"'; break; | |
case is_nan($a): $this->line .= '"n`NAN"'; break; | |
case $a > 9007199254740992 && is_int($a): $a = '"n`' . $a . '"'; // JavaScript max integer is 2^53 | |
default: $this->line .= (string) $a; break; | |
} | |
} | |
protected function dumpString($a, $is_key) | |
{ | |
if ($is_key) | |
{ | |
$is_key = $this->lastHash === $this->counter && !isset($this->depthLimited[$this->counter]); | |
$this->dumpLine(-$is_key, $this->line .= ','); | |
$is_key = ': '; | |
} | |
else $is_key = ''; | |
if ('' === $a) return $this->line .= '""' . $is_key; | |
if (!preg_match("''u", $a)) $a = 'b`' . utf8_encode($a); | |
else if (false !== strpos($a, '`')) $a = 'u`' . $a; | |
if (0 < $this->maxString && $this->maxString < $len = iconv_strlen($a, 'UTF-8') - 1) | |
$a = $len . ('`' !== substr($a, 1, 1) ? 'u`' : '') . substr($a, 0, $this->maxString + 1); | |
$this->line .= '"' . str_replace( | |
array( | |
'\\', '"', '</', | |
"\x00", "\x01", "\x02", "\x03", "\x04", "\x05", "\x06", "\x07", | |
"\x08", "\x09", "\x0A", "\x0B", "\x0C", "\x0D", "\x0E", "\x0F", | |
"\x10", "\x11", "\x12", "\x13", "\x14", "\x15", "\x16", "\x17", | |
"\x18", "\x19", "\x1A", "\x1B", "\x1C", "\x1D", "\x1E", "\x1F", | |
), | |
array( | |
'\\\\', '\\"', '<\\/', | |
'\u0000','\u0001','\u0002','\u0003','\u0004','\u0005','\u0006','\u0007', | |
'\b' ,'\t' ,'\n' ,'\u000B','\f' ,'\r' ,'\u000E','\u000F', | |
'\u0010','\u0011','\u0012','\u0013','\u0014','\u0015','\u0016','\u0017', | |
'\u0018','\u0019','\u001A','\u001B','\u001C','\u001D','\u001E','\u001F', | |
), | |
$a | |
) . '"' . $is_key; | |
} | |
protected function walkHash($type, &$a) | |
{ | |
if ('array:0' === $type) $this->line .= '[]'; | |
else | |
{ | |
$h = $this->lastHash; | |
$this->line .= '{"_":'; | |
$this->lastHash = $this->counter; | |
$this->dumpString($this->counter . ':' . $type, false); | |
if ($type = parent::walkHash($type, $a)) | |
{ | |
++$this->depth; | |
$this->dumpString('__refs', true); | |
$this->line .= '{'; | |
foreach ($type as $k => &$a) $a = '"' . $k . '":[' . implode(',', $a) . ']'; | |
$this->line .= implode(',', $type) . '}'; | |
--$this->depth; | |
} | |
if ($this->counter !== $this->lastHash || isset($this->depthLimited[$this->counter])) | |
$this->dumpLine(1); | |
$this->lastHash = $h; | |
$this->line .= '}'; | |
} | |
} | |
static function echoLine($line, $depth) | |
{ | |
echo str_repeat(' ', $depth), $line, "\n"; | |
} | |
protected function stackLine($line, $depth) | |
{ | |
$this->lines[] = str_repeat(' ', $depth) . $line; | |
} | |
} |
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
<?php /****************** vi: set fenc=utf-8 ts=4 sw=4 et: ***************** | |
* | |
* Copyright : (C) 2011 Nicolas Grekas. All rights reserved. | |
* Email : [email protected] | |
* License : http://www.gnu.org/licenses/lgpl.txt GNU/LGPL | |
* | |
* This program is free software; you can redistribute it and/or modify | |
* it under the terms of the GNU Lesser General Public License as | |
* published by the Free Software Foundation, either version 3 of the | |
* License, or (at your option) any later version. | |
* | |
***************************************************************************/ | |
namespace Patchwork\PHP; | |
/** | |
* Logger logs messages to an output stream. | |
* | |
* Messages just have a type and associated data. The dump format is handled by JsonDumper | |
* which allows unprecedented accuracy for associated data representation. | |
* | |
* Error messages are handled specifically in order to make them more friendly, | |
* especially for traces and exceptions. | |
*/ | |
class Logger | |
{ | |
public | |
$writeLock = true, | |
$lineFormat = "%s\n", | |
$loggedGlobals = array('_SERVER'); | |
protected | |
$logStream, | |
$prevTime = 0, | |
$startTime = 0, | |
$isFirstEvent = true; | |
public static | |
$errorTypes = array( | |
E_RECOVERABLE_ERROR => 'E_RECOVERABLE_ERROR', | |
E_DEPRECATED => 'E_DEPRECATED', | |
E_USER_DEPRECATED => 'E_USER_DEPRECATED', | |
E_ERROR => 'E_ERROR', | |
E_WARNING => 'E_WARNING', | |
E_PARSE => 'E_PARSE', | |
E_NOTICE => 'E_NOTICE', | |
E_CORE_ERROR => 'E_CORE_ERROR', | |
E_CORE_WARNING => 'E_CORE_WARNING', | |
E_COMPILE_ERROR => 'E_COMPILE_ERROR', | |
E_COMPILE_WARNING => 'E_COMPILE_WARNING', | |
E_USER_ERROR => 'E_USER_ERROR', | |
E_USER_WARNING => 'E_USER_WARNING', | |
E_USER_NOTICE => 'E_USER_NOTICE', | |
E_STRICT => 'E_STRICT', | |
); | |
function __construct($log_stream, $start_time = 0) | |
{ | |
$start_time || $start_time = microtime(true); | |
$this->startTime = $this->prevTime = $start_time; | |
$this->logStream = $log_stream; | |
} | |
function log($type, $data, $log_time = 0) | |
{ | |
// Get time and memory profiling information | |
$log_time || $log_time = microtime(true); | |
$data = array( | |
'time' => date('c', $log_time) . sprintf( | |
' %06dus - %0.3fms - %0.3fms', | |
100000 * ($log_time - floor($log_time)), | |
1000 * ($log_time - $this->startTime), | |
1000 * ($log_time - $this->prevTime) | |
), | |
'mem' => memory_get_peak_usage(true) . ' - ' . memory_get_usage(true), | |
'data' => $data, | |
); | |
if ($this->isFirstEvent && $this->loggedGlobals) | |
{ | |
$data['globals'] = array(); | |
foreach ($this->loggedGlobals as $log_time) | |
$data['globals'][$log_time] = isset($GLOBALS[$log_time]) ? $GLOBALS[$log_time] : null; | |
} | |
$this->writeLock && flock($this->logStream, LOCK_EX); | |
$this->writeEvent($type, $data); | |
$this->writeLock && flock($this->logStream, LOCK_UN); | |
$this->prevTime = microtime(true); | |
$this->isFirstEvent = false; | |
} | |
function logError($e, $trace_offset = -1, $trace_args = 0, $log_time = 0) | |
{ | |
$e = array( | |
'mesg' => $e['message'], | |
'type' => self::$errorTypes[$e['type']] . ' ' . $e['file'] . ':' . $e['line'], | |
) + $e; | |
unset($e['message'], $e['file'], $e['line']); | |
if (0 > $trace_offset) unset($e['trace']); | |
else if (!empty($e['trace'])) $e['trace'] = $this->filterTrace($e['trace'], $trace_offset, $trace_args); | |
$this->log('php-error', $e, $log_time); | |
} | |
function castException($e) | |
{ | |
$a = (array) $e; | |
$a["\0Exception\0trace"] = $this->filterTrace($a["\0Exception\0trace"], $e instanceof RecoverableErrorInterface ? $e->traceOffset : 0, 1); | |
if (null === $a["\0Exception\0trace"]) unset($a["\0Exception\0trace"]); | |
if ($e instanceof RecoverableErrorInterface) unset($a['traceOffset']); | |
if (empty($a["\0Exception\0previous"])) unset($a["\0Exception\0previous"]); | |
if ($e instanceof \ErrorException && isset(self::$errorTypes[$a["\0*\0severity"]])) $a["\0*\0severity"] = self::$errorTypes[$a["\0*\0severity"]]; | |
unset($a["\0Exception\0string"], $a['xdebug_message'], $a['__destructorException']); | |
return $a; | |
} | |
function filterTrace($trace, $offset, $args) | |
{ | |
if (0 > $offset || empty($trace[$offset])) return null; | |
else $t = $trace[$offset]; | |
if (empty($t['class']) && isset($t['function'])) | |
if ('user_error' === $t['function'] || 'trigger_error' === $t['function']) | |
++$offset; | |
$offset && array_splice($trace, 0, $offset); | |
foreach ($trace as &$t) | |
{ | |
$t = array( | |
'call' => (isset($t['class']) ? $t['class'] . $t['type'] : '') | |
. $t['function'] . '()' | |
. (isset($t['line']) ? " {$t['file']}:{$t['line']}" : '') | |
) + $t; | |
unset($t['class'], $t['type'], $t['function'], $t['file'], $t['line']); | |
if (isset($t['args']) && !$args) unset($t['args']); | |
} | |
return $trace; | |
} | |
function writeEvent($type, $data) | |
{ | |
fprintf($this->logStream, $this->lineFormat, "*** {$type} ***"); | |
$d = new JsonDumper; | |
$d->setCallback('line', array($this, 'writeLine')); | |
$d->setCallback('o:exception', array($this, 'castException')); | |
$d->walk($data); | |
fprintf($this->logStream, $this->lineFormat, '***'); | |
} | |
function writeLine($line, $depth) | |
{ | |
fprintf($this->logStream, $this->lineFormat, str_repeat(' ', $depth) . $line); | |
} | |
} |
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
<?php /****************** vi: set fenc=utf-8 ts=4 sw=4 et: ***************** | |
* | |
* Copyright : (C) 2011 Nicolas Grekas. All rights reserved. | |
* Email : [email protected] | |
* License : http://www.gnu.org/licenses/lgpl.txt GNU/LGPL | |
* | |
* This program is free software; you can redistribute it and/or modify | |
* it under the terms of the GNU Lesser General Public License as | |
* published by the Free Software Foundation, either version 3 of the | |
* License, or (at your option) any later version. | |
* | |
***************************************************************************/ | |
namespace Patchwork\PHP; | |
/** | |
* Walker implements a mechanism to generically traverse any PHP variable. | |
* | |
* It takes internal references into account, recursive or non-recursive, without preempting any | |
* special use of the discovered data. It exposes only one public method ->walk(), which triggers | |
* the traversal. It also has a public property ->checkInternalRefs set to true by default, to | |
* disable the check for internal references if the mechanism is considered too expensive. | |
* Checking recursive references and object/resource can not be disabled but is much lighter. | |
*/ | |
abstract class Walker | |
{ | |
public | |
$checkInternalRefs = true; | |
protected | |
$tag, | |
$token, | |
$depth = 0, | |
$counter = 0, | |
$arrayType = 0, | |
$refMap = array(), | |
$refPool = array(), | |
$valPool = array(), | |
$objPool = array(), | |
$arrayPool = array(); | |
abstract protected function dumpRef($is_soft, $ref_counter = null, &$ref_value = null); | |
abstract protected function dumpScalar($val); | |
abstract protected function dumpString($str, $is_key); | |
abstract protected function dumpObject($obj); | |
abstract protected function dumpResource($res); | |
function walk(&$a) | |
{ | |
$this->tag = (object) array(); | |
$this->token = md5(mt_rand() . spl_object_hash($this->tag), true); | |
$this->tag = array($this->token => $this->tag); | |
$this->counter = $this->depth = 0; | |
$this->walkRef($a); | |
} | |
protected function walkRef(&$a) | |
{ | |
++$this->counter; | |
if (is_array($a)) return $this->walkArray($a); | |
$v = $a; | |
if ($this->checkInternalRefs && 1 < $this->counter) | |
{ | |
$this->refPool[$this->counter] =& $a; | |
$this->valPool[$this->counter] = $a; | |
$a = $this->tag; | |
} | |
switch (true) | |
{ | |
default: $this->dumpScalar($v); break; | |
case is_string($v): $this->dumpString($v, false); break; | |
case is_object($v): $h = pack('H*', spl_object_hash($v)); // no break; | |
case is_resource($v): isset($h) || $h = (int) substr((string) $v, 13); | |
if (empty($this->objPool[$h])) $this->objPool[$h] = $this->counter; | |
else return $this->dumpRef(true, $this->refMap[$this->counter] = $this->objPool[$h], $v); | |
$t = $this->arrayType; | |
$this->arrayType = 0; | |
if (isset($h[0])) $this->dumpObject($v); | |
else $this->dumpResource($v); | |
$this->arrayType = $t; | |
} | |
} | |
protected function walkArray(&$a) | |
{ | |
if (isset($a[$this->token])) | |
{ | |
if ($this->tag[$this->token] === $c = $a[$this->token]) | |
{ | |
if (empty($a['ref_counter'])) | |
{ | |
$a[] = -$this->counter; | |
return $this->dumpRef(false); | |
} | |
$c = $a['ref_counter']; | |
unset($a); | |
$a = $this->valPool[$c]; | |
} | |
$this->refMap[-$this->counter] = $c; | |
return $this->dumpRef(false, $c, $a); | |
} | |
if ($this->checkInternalRefs) $token = $this->token; | |
else | |
{ | |
/**/ if (PHP_VERSION_ID >= 50206) | |
/**/ { | |
if (0 === $this->arrayType) | |
{ | |
// Detect recursive arrays by catching recursive count warnings | |
$this->arrayType = 1; | |
set_error_handler(array($this, 'catchRecursionWarning')); | |
count($a, COUNT_RECURSIVE); | |
restore_error_handler(); | |
} | |
if (2 === $this->arrayType) $token = $this->token; | |
/**/ } | |
/**/ else | |
/**/ { | |
$token = $this->token; | |
/**/ } | |
} | |
$len = count($a); | |
if (isset($token)) | |
{ | |
$a[$token] = $this->counter; | |
$this->arrayPool[] =& $a; | |
} | |
$this->walkHash('array:' . $len, $a); | |
} | |
protected function walkHash($type, &$a) | |
{ | |
++$this->depth; | |
foreach ($a as $k => &$a) | |
{ | |
if ($k === $this->token) continue; | |
$this->dumpString($k, true); | |
$this->walkRef($a); | |
} | |
if (--$this->depth) return array(); | |
else return $this->cleanRefPools(); | |
} | |
protected function cleanRefPools() | |
{ | |
$refs = array(); | |
foreach ($this->refPool as $k => &$v) | |
{ | |
$len = $v; | |
$v = $this->valPool[$k]; | |
if (isset($len[0])) | |
{ | |
unset($len['ref_counter']); | |
$refs[$k] = array_slice($len, 1); | |
} | |
} | |
$this->refPool = $this->valPool = $this->objPool = array(); | |
foreach ($this->refMap as $len => $k) $refs[$k][] = $len; | |
foreach ($this->arrayPool as &$a) unset($a[$this->token]); | |
$this->arrayPool = $this->refMap = array(); | |
return $refs; | |
} | |
protected function catchRecursionWarning() | |
{ | |
$this->arrayType = 2; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment