use Symfony\Component\Debug\Exception\FatalErrorException;
use Symfony\Component\Debug\Exception\FatalThrowableError;
use Symfony\Component\Debug\Exception\OutOfMemoryException;
+use Symfony\Component\Debug\Exception\SilencedErrorContext;
use Symfony\Component\Debug\FatalErrorHandler\UndefinedFunctionFatalErrorHandler;
use Symfony\Component\Debug\FatalErrorHandler\UndefinedMethodFatalErrorHandler;
use Symfony\Component\Debug\FatalErrorHandler\ClassNotFoundFatalErrorHandler;
* - thrownErrors: errors thrown as \ErrorException
* - loggedErrors: logged errors, when not @-silenced
* - scopedErrors: errors thrown or logged with their local context
- * - tracedErrors: errors logged with their stack trace, only once for repeated errors
+ * - tracedErrors: errors logged with their stack trace
* - screamedErrors: never @-silenced errors
*
* Each error level can be logged by a dedicated PSR-3 logger object.
* can see them and weight them as more important to fix than others of the same level.
*
* @author Nicolas Grekas <p@tchwork.com>
+ * @author Grégoire Pineau <lyrixx@lyrixx.info>
*/
class ErrorHandler
{
- /**
- * @deprecated since version 2.6, to be removed in 3.0.
- */
- const TYPE_DEPRECATION = -100;
-
private $levels = array(
E_DEPRECATED => 'Deprecated',
E_USER_DEPRECATED => 'User Deprecated',
private $tracedErrors = 0x77FB; // E_ALL - E_STRICT - E_PARSE
private $screamedErrors = 0x55; // E_ERROR + E_CORE_ERROR + E_COMPILE_ERROR + E_PARSE
private $loggedErrors = 0;
+ private $traceReflector;
- private $loggedTraces = array();
private $isRecursive = 0;
private $isRoot = false;
private $exceptionHandler;
private static $stackedErrors = array();
private static $stackedErrorLevels = array();
private static $toStringException = null;
+ private static $silencedErrorCache = array();
+ private static $silencedErrorCount = 0;
private static $exitCode = 0;
- /**
- * Same init value as thrownErrors.
- *
- * @deprecated since version 2.6, to be removed in 3.0.
- */
- private $displayErrors = 0x1FFF;
-
/**
* Registers the error handler.
*
- * @param self|null|int $handler The handler to register, or @deprecated (since version 2.6, to be removed in 3.0) bit field of thrown levels
- * @param bool $replace Whether to replace or not any existing handler
+ * @param self|null $handler The handler to register
+ * @param bool $replace Whether to replace or not any existing handler
*
* @return self The registered error handler
*/
- public static function register($handler = null, $replace = true)
+ public static function register(self $handler = null, $replace = true)
{
if (null === self::$reservedMemory) {
self::$reservedMemory = str_repeat('x', 10240);
register_shutdown_function(__CLASS__.'::handleFatalError');
}
- $levels = -1;
-
- if ($handlerIsNew = !$handler instanceof self) {
- // @deprecated polymorphism, to be removed in 3.0
- if (null !== $handler) {
- $levels = $replace ? $handler : 0;
- $replace = true;
- }
+ if ($handlerIsNew = null === $handler) {
$handler = new static();
}
$handler = $prev[0];
$replace = false;
}
- if ($replace || !$prev) {
- $handler->setExceptionHandler(set_exception_handler(array($handler, 'handleException')));
- } else {
+ if (!$replace && $prev) {
restore_error_handler();
+ $handlerIsRegistered = is_array($prev) && $handler === $prev[0];
+ } else {
+ $handlerIsRegistered = true;
+ }
+ if (is_array($prev = set_exception_handler(array($handler, 'handleException'))) && $prev[0] instanceof self) {
+ restore_exception_handler();
+ if (!$handlerIsRegistered) {
+ $handler = $prev[0];
+ } elseif ($handler !== $prev[0] && $replace) {
+ set_exception_handler(array($handler, 'handleException'));
+ $p = $prev[0]->setExceptionHandler(null);
+ $handler->setExceptionHandler($p);
+ $prev[0]->setExceptionHandler($p);
+ }
+ } else {
+ $handler->setExceptionHandler($prev);
}
- $handler->throwAt($levels & $handler->thrownErrors, true);
+ $handler->throwAt(E_ALL & $handler->thrownErrors, true);
return $handler;
}
$this->bootstrappingLogger = $bootstrappingLogger;
$this->setDefaultLogger($bootstrappingLogger);
}
+ $this->traceReflector = new \ReflectionProperty('Exception', 'trace');
+ $this->traceReflector->setAccessible(true);
}
/**
* @param array|int $levels An array map of E_* to LogLevel::* or an integer bit field of E_* constants
* @param bool $replace Whether to replace or not any existing logger
*/
- public function setDefaultLogger(LoggerInterface $logger, $levels = null, $replace = false)
+ public function setDefaultLogger(LoggerInterface $logger, $levels = E_ALL, $replace = false)
{
$loggers = array();
}
} else {
if (null === $levels) {
- $levels = E_ALL | E_STRICT;
+ $levels = E_ALL;
}
foreach ($this->loggers as $type => $log) {
if (($type & $levels) && (empty($log[0]) || $replace || $log[0] === $this->bootstrappingLogger)) {
if ($flush) {
foreach ($this->bootstrappingLogger->cleanLogs() as $log) {
- $type = $log[2]['type'];
+ $type = $log[2]['exception'] instanceof \ErrorException ? $log[2]['exception']->getSeverity() : E_ERROR;
if (!isset($flush[$type])) {
$this->bootstrappingLogger->log($log[0], $log[1], $log[2]);
} elseif ($this->loggers[$type][0]) {
* @param callable $handler A handler that will be called on Exception
*
* @return callable|null The previous exception handler
- *
- * @throws \InvalidArgumentException
*/
- public function setExceptionHandler($handler)
+ public function setExceptionHandler(callable $handler = null)
{
- if (null !== $handler && !is_callable($handler)) {
- throw new \LogicException('The exception handler must be a valid PHP callable.');
- }
$prev = $this->exceptionHandler;
$this->exceptionHandler = $handler;
}
$this->reRegister($prev | $this->loggedErrors);
- // $this->displayErrors is @deprecated since version 2.6
- $this->displayErrors = $this->thrownErrors;
-
return $prev;
}
*/
public function handleError($type, $message, $file, $line)
{
+ // Level is the current error reporting level to manage silent error.
+ // Strong errors are not authorized to be silenced.
$level = error_reporting() | E_RECOVERABLE_ERROR | E_USER_ERROR | E_DEPRECATED | E_USER_DEPRECATED;
$log = $this->loggedErrors & $type;
$throw = $this->thrownErrors & $type & $level;
return true;
}
- if ($throw) {
- if (null !== self::$toStringException) {
- $throw = self::$toStringException;
- self::$toStringException = null;
- } elseif ($scope && class_exists('Symfony\Component\Debug\Exception\ContextErrorException')) {
- // Checking for class existence is a work around for https://bugs.php.net/42098
- $throw = new ContextErrorException($this->levels[$type].': '.$message, 0, $type, $file, $line, $context);
+ $logMessage = $this->levels[$type].': '.$message;
+
+ if (null !== self::$toStringException) {
+ $errorAsException = self::$toStringException;
+ self::$toStringException = null;
+ } elseif (!$throw && !($type & $level)) {
+ if (!isset(self::$silencedErrorCache[$id = $file.':'.$line])) {
+ $lightTrace = $this->tracedErrors & $type ? $this->cleanTrace(debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 3), $type, $file, $line, false) : array();
+ $errorAsException = new SilencedErrorContext($type, $file, $line, $lightTrace);
+ } elseif (isset(self::$silencedErrorCache[$id][$message])) {
+ $lightTrace = null;
+ $errorAsException = self::$silencedErrorCache[$id][$message];
+ ++$errorAsException->count;
} else {
- $throw = new \ErrorException($this->levels[$type].': '.$message, 0, $type, $file, $line);
+ $lightTrace = array();
+ $errorAsException = null;
}
- if (\PHP_VERSION_ID <= 50407 && (\PHP_VERSION_ID >= 50400 || \PHP_VERSION_ID <= 50317)) {
- // Exceptions thrown from error handlers are sometimes not caught by the exception
- // handler and shutdown handlers are bypassed before 5.4.8/5.3.18.
- // We temporarily re-enable display_errors to prevent any blank page related to this bug.
+ if (100 < ++self::$silencedErrorCount) {
+ self::$silencedErrorCache = $lightTrace = array();
+ self::$silencedErrorCount = 1;
+ }
+ if ($errorAsException) {
+ self::$silencedErrorCache[$id][$message] = $errorAsException;
+ }
+ if (null === $lightTrace) {
+ return;
+ }
+ } else {
+ if ($scope) {
+ $errorAsException = new ContextErrorException($logMessage, 0, $type, $file, $line, $context);
+ } else {
+ $errorAsException = new \ErrorException($logMessage, 0, $type, $file, $line);
+ }
- $throw->errorHandlerCanary = new ErrorHandlerCanary();
+ // Clean the trace by removing function arguments and the first frames added by the error handler itself.
+ if ($throw || $this->tracedErrors & $type) {
+ $backtrace = $backtrace ?: $errorAsException->getTrace();
+ $lightTrace = $this->cleanTrace($backtrace, $type, $file, $line, $throw);
+ $this->traceReflector->setValue($errorAsException, $lightTrace);
+ } else {
+ $this->traceReflector->setValue($errorAsException, array());
}
+ }
+ if ($throw) {
if (E_USER_ERROR & $type) {
- $backtrace = $backtrace ?: $throw->getTrace();
-
for ($i = 1; isset($backtrace[$i]); ++$i) {
if (isset($backtrace[$i]['function'], $backtrace[$i]['type'], $backtrace[$i - 1]['function'])
&& '__toString' === $backtrace[$i]['function']
if (($e instanceof \Exception || $e instanceof \Throwable) && $e->__toString() === $message) {
if (1 === $i) {
// On HHVM
- $throw = $e;
+ $errorAsException = $e;
break;
}
self::$toStringException = $e;
if (1 < $i) {
// On PHP (not on HHVM), display the original error message instead of the default one.
- $this->handleException($throw);
+ $this->handleException($errorAsException);
// Stop the process by giving back the error to the native handler.
return false;
}
}
- throw $throw;
- }
-
- // For duplicated errors, log the trace only once
- $e = md5("{$type}/{$line}/{$file}\x00{$message}", true);
- $trace = true;
-
- if (!($this->tracedErrors & $type) || isset($this->loggedTraces[$e])) {
- $trace = false;
- } else {
- $this->loggedTraces[$e] = 1;
- }
-
- $e = compact('type', 'file', 'line', 'level');
-
- if ($type & $level) {
- if ($scope) {
- $e['scope_vars'] = $context;
- if ($trace) {
- $e['stack'] = $backtrace ?: debug_backtrace(DEBUG_BACKTRACE_PROVIDE_OBJECT);
- }
- } elseif ($trace) {
- if (null === $backtrace) {
- $e['stack'] = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
- } else {
- foreach ($backtrace as &$frame) {
- unset($frame['args'], $frame);
- }
- $e['stack'] = $backtrace;
- }
- }
+ throw $errorAsException;
}
if ($this->isRecursive) {
$log = 0;
} elseif (self::$stackedErrorLevels) {
- self::$stackedErrors[] = array($this->loggers[$type][0], ($type & $level) ? $this->loggers[$type][1] : LogLevel::DEBUG, $message, $e);
+ self::$stackedErrors[] = array(
+ $this->loggers[$type][0],
+ ($type & $level) ? $this->loggers[$type][1] : LogLevel::DEBUG,
+ $logMessage,
+ $errorAsException ? array('exception' => $errorAsException) : array(),
+ );
} else {
try {
$this->isRecursive = true;
- $this->loggers[$type][0]->log(($type & $level) ? $this->loggers[$type][1] : LogLevel::DEBUG, $message, $e);
+ $level = ($type & $level) ? $this->loggers[$type][1] : LogLevel::DEBUG;
+ $this->loggers[$type][0]->log($level, $logMessage, $errorAsException ? array('exception' => $errorAsException) : array());
+ } finally {
$this->isRecursive = false;
- } catch (\Exception $e) {
- $this->isRecursive = false;
-
- throw $e;
- } catch (\Throwable $e) {
- $this->isRecursive = false;
-
- throw $e;
}
}
$exception = new FatalThrowableError($exception);
}
$type = $exception instanceof FatalErrorException ? $exception->getSeverity() : E_ERROR;
+ $handlerException = null;
if (($this->loggedErrors & $type) || $exception instanceof FatalThrowableError) {
- $e = array(
- 'type' => $type,
- 'file' => $exception->getFile(),
- 'line' => $exception->getLine(),
- 'level' => error_reporting(),
- 'stack' => $exception->getTrace(),
- );
if ($exception instanceof FatalErrorException) {
if ($exception instanceof FatalThrowableError) {
$error = array(
'type' => $type,
'message' => $message = $exception->getMessage(),
- 'file' => $e['file'],
- 'line' => $e['line'],
+ 'file' => $exception->getFile(),
+ 'line' => $exception->getLine(),
);
} else {
$message = 'Fatal '.$exception->getMessage();
}
} elseif ($exception instanceof \ErrorException) {
$message = 'Uncaught '.$exception->getMessage();
- if ($exception instanceof ContextErrorException) {
- $e['context'] = $exception->getContext();
- }
} else {
$message = 'Uncaught Exception: '.$exception->getMessage();
}
}
if ($this->loggedErrors & $type) {
try {
- $this->loggers[$type][0]->log($this->loggers[$type][1], $message, $e);
+ $this->loggers[$type][0]->log($this->loggers[$type][1], $message, array('exception' => $exception));
} catch (\Exception $handlerException) {
} catch (\Throwable $handlerException) {
}
}
}
}
- if (empty($this->exceptionHandler)) {
- throw $exception; // Give back $exception to the native handler
- }
try {
- call_user_func($this->exceptionHandler, $exception);
+ if (null !== $this->exceptionHandler) {
+ return \call_user_func($this->exceptionHandler, $exception);
+ }
+ $handlerException = $handlerException ?: $exception;
} catch (\Exception $handlerException) {
} catch (\Throwable $handlerException) {
}
- if (isset($handlerException)) {
- $this->exceptionHandler = null;
- $this->handleException($handlerException);
+ $this->exceptionHandler = null;
+ if ($exception === $handlerException) {
+ self::$reservedMemory = null; // Disable the fatal error handler
+ throw $exception; // Give back $exception to the native handler
}
+ $this->handleException($handlerException);
}
/**
return;
}
- self::$reservedMemory = null;
+ $handler = self::$reservedMemory = null;
+ $handlers = array();
+ $previousHandler = null;
+ $sameHandlerLimit = 10;
- $handler = set_error_handler('var_dump');
- $handler = is_array($handler) ? $handler[0] : null;
- restore_error_handler();
+ while (!is_array($handler) || !$handler[0] instanceof self) {
+ $handler = set_exception_handler('var_dump');
+ restore_exception_handler();
- if (!$handler instanceof self) {
+ if (!$handler) {
+ break;
+ }
+ restore_exception_handler();
+
+ if ($handler !== $previousHandler) {
+ array_unshift($handlers, $handler);
+ $previousHandler = $handler;
+ } elseif (0 === --$sameHandlerLimit) {
+ $handler = null;
+ break;
+ }
+ }
+ foreach ($handlers as $h) {
+ set_exception_handler($h);
+ }
+ if (!$handler) {
return;
}
+ if ($handler !== $h) {
+ $handler[0]->setExceptionHandler($h);
+ }
+ $handler = $handler[0];
+ $handlers = array();
if ($exit = null === $error) {
$error = error_get_last();
*
* The most important feature of this is to prevent
* autoloading until unstackErrors() is called.
+ *
+ * @deprecated since version 3.4, to be removed in 4.0.
*/
public static function stackErrors()
{
+ @trigger_error('Support for stacking errors is deprecated since Symfony 3.4 and will be removed in 4.0.', E_USER_DEPRECATED);
+
self::$stackedErrorLevels[] = error_reporting(error_reporting() | E_PARSE | E_ERROR | E_CORE_ERROR | E_COMPILE_ERROR);
}
/**
* Unstacks stacked errors and forwards to the logger.
+ *
+ * @deprecated since version 3.4, to be removed in 4.0.
*/
public static function unstackErrors()
{
+ @trigger_error('Support for unstacking errors is deprecated since Symfony 3.4 and will be removed in 4.0.', E_USER_DEPRECATED);
+
$level = array_pop(self::$stackedErrorLevels);
if (null !== $level) {
- $e = error_reporting($level);
- if ($e !== ($level | E_PARSE | E_ERROR | E_CORE_ERROR | E_COMPILE_ERROR)) {
+ $errorReportingLevel = error_reporting($level);
+ if ($errorReportingLevel !== ($level | E_PARSE | E_ERROR | E_CORE_ERROR | E_COMPILE_ERROR)) {
// If the user changed the error level, do not overwrite it
- error_reporting($e);
+ error_reporting($errorReportingLevel);
}
}
$errors = self::$stackedErrors;
self::$stackedErrors = array();
- foreach ($errors as $e) {
- $e[0]->log($e[1], $e[2], $e[3]);
+ foreach ($errors as $error) {
+ $error[0]->log($error[1], $error[2], $error[3]);
}
}
}
);
}
- /**
- * Sets the level at which the conversion to Exception is done.
- *
- * @param int|null $level The level (null to use the error_reporting() value and 0 to disable)
- *
- * @deprecated since version 2.6, to be removed in 3.0. Use throwAt() instead.
- */
- public function setLevel($level)
+ private function cleanTrace($backtrace, $type, $file, $line, $throw)
{
- @trigger_error('The '.__METHOD__.' method is deprecated since version 2.6 and will be removed in 3.0. Use the throwAt() method instead.', E_USER_DEPRECATED);
-
- $level = null === $level ? error_reporting() : $level;
- $this->throwAt($level, true);
- }
+ $lightTrace = $backtrace;
- /**
- * Sets the display_errors flag value.
- *
- * @param int $displayErrors The display_errors flag value
- *
- * @deprecated since version 2.6, to be removed in 3.0. Use throwAt() instead.
- */
- public function setDisplayErrors($displayErrors)
- {
- @trigger_error('The '.__METHOD__.' method is deprecated since version 2.6 and will be removed in 3.0. Use the throwAt() method instead.', E_USER_DEPRECATED);
-
- if ($displayErrors) {
- $this->throwAt($this->displayErrors, true);
- } else {
- $displayErrors = $this->displayErrors;
- $this->throwAt(0, true);
- $this->displayErrors = $displayErrors;
- }
- }
-
- /**
- * Sets a logger for the given channel.
- *
- * @param LoggerInterface $logger A logger interface
- * @param string $channel The channel associated with the logger (deprecation, emergency or scream)
- *
- * @deprecated since version 2.6, to be removed in 3.0. Use setLoggers() or setDefaultLogger() instead.
- */
- public static function setLogger(LoggerInterface $logger, $channel = 'deprecation')
- {
- @trigger_error('The '.__METHOD__.' static method is deprecated since version 2.6 and will be removed in 3.0. Use the setLoggers() or setDefaultLogger() methods instead.', E_USER_DEPRECATED);
-
- $handler = set_error_handler('var_dump');
- $handler = is_array($handler) ? $handler[0] : null;
- restore_error_handler();
- if (!$handler instanceof self) {
- return;
- }
- if ('deprecation' === $channel) {
- $handler->setDefaultLogger($logger, E_DEPRECATED | E_USER_DEPRECATED, true);
- $handler->screamAt(E_DEPRECATED | E_USER_DEPRECATED);
- } elseif ('scream' === $channel) {
- $handler->setDefaultLogger($logger, E_ALL | E_STRICT, false);
- $handler->screamAt(E_ALL | E_STRICT);
- } elseif ('emergency' === $channel) {
- $handler->setDefaultLogger($logger, E_PARSE | E_ERROR | E_CORE_ERROR | E_COMPILE_ERROR, true);
- $handler->screamAt(E_PARSE | E_ERROR | E_CORE_ERROR | E_COMPILE_ERROR);
+ for ($i = 0; isset($backtrace[$i]); ++$i) {
+ if (isset($backtrace[$i]['file'], $backtrace[$i]['line']) && $backtrace[$i]['line'] === $line && $backtrace[$i]['file'] === $file) {
+ $lightTrace = array_slice($lightTrace, 1 + $i);
+ break;
+ }
}
- }
-
- /**
- * @deprecated since version 2.6, to be removed in 3.0. Use handleError() instead.
- */
- public function handle($level, $message, $file = 'unknown', $line = 0, $context = array())
- {
- $this->handleError(E_USER_DEPRECATED, 'The '.__METHOD__.' method is deprecated since version 2.6 and will be removed in 3.0. Use the handleError() method instead.', __FILE__, __LINE__, array());
-
- return $this->handleError($level, $message, $file, $line, (array) $context);
- }
-
- /**
- * Handles PHP fatal errors.
- *
- * @deprecated since version 2.6, to be removed in 3.0. Use handleFatalError() instead.
- */
- public function handleFatal()
- {
- @trigger_error('The '.__METHOD__.' method is deprecated since version 2.6 and will be removed in 3.0. Use the handleFatalError() method instead.', E_USER_DEPRECATED);
-
- static::handleFatalError();
- }
-}
-
-/**
- * Private class used to work around https://bugs.php.net/54275.
- *
- * @author Nicolas Grekas <p@tchwork.com>
- *
- * @internal
- */
-class ErrorHandlerCanary
-{
- private static $displayErrors = null;
-
- public function __construct()
- {
- if (null === self::$displayErrors) {
- self::$displayErrors = ini_set('display_errors', 1);
+ if (!($throw || $this->scopedErrors & $type)) {
+ for ($i = 0; isset($lightTrace[$i]); ++$i) {
+ unset($lightTrace[$i]['args'], $lightTrace[$i]['object']);
+ }
}
- }
- public function __destruct()
- {
- if (null !== self::$displayErrors) {
- ini_set('display_errors', self::$displayErrors);
- self::$displayErrors = null;
- }
+ return $lightTrace;
}
}