Skip to content

Instantly share code, notes, and snippets.

@nicolas-grekas
Created July 7, 2011 16:58
Show Gist options
  • Star 8 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save nicolas-grekas/1069975 to your computer and use it in GitHub Desktop.
Save nicolas-grekas/1069975 to your computer and use it in GitHub Desktop.
Advanced error handling in PHP
<?php /****************** vi: set fenc=utf-8 ts=4 sw=4 et: *****************
*
* Copyright : (C) 2011 Nicolas Grekas. All rights reserved.
* Email : p@tchwork.org
* 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;
}
}
<?php /****************** vi: set fenc=utf-8 ts=4 sw=4 et: *****************
*
* Copyright : (C) 2011 Nicolas Grekas. All rights reserved.
* Email : p@tchwork.org
* 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 {}
<?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(),
));
}
<?php /****************** vi: set fenc=utf-8 ts=4 sw=4 et: *****************
*
* Copyright : (C) 2011 Nicolas Grekas. All rights reserved.
* Email : p@tchwork.org
* 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;
}
}
<?php /****************** vi: set fenc=utf-8 ts=4 sw=4 et: *****************
*
* Copyright : (C) 2011 Nicolas Grekas. All rights reserved.
* Email : p@tchwork.org
* 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);
}
}
<?php /****************** vi: set fenc=utf-8 ts=4 sw=4 et: *****************
*
* Copyright : (C) 2011 Nicolas Grekas. All rights reserved.
* Email : p@tchwork.org
* 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