Chris@0: Chris@0: * Chris@0: * For the full copyright and license information, please view the LICENSE Chris@0: * file that was distributed with this source code. Chris@0: */ Chris@0: Chris@0: namespace Symfony\Component\Debug; Chris@0: Chris@17: use Psr\Log\LoggerInterface; Chris@0: use Psr\Log\LogLevel; Chris@0: use Symfony\Component\Debug\Exception\ContextErrorException; Chris@0: use Symfony\Component\Debug\Exception\FatalErrorException; Chris@0: use Symfony\Component\Debug\Exception\FatalThrowableError; Chris@0: use Symfony\Component\Debug\Exception\OutOfMemoryException; Chris@0: use Symfony\Component\Debug\Exception\SilencedErrorContext; Chris@17: use Symfony\Component\Debug\FatalErrorHandler\ClassNotFoundFatalErrorHandler; Chris@17: use Symfony\Component\Debug\FatalErrorHandler\FatalErrorHandlerInterface; Chris@0: use Symfony\Component\Debug\FatalErrorHandler\UndefinedFunctionFatalErrorHandler; Chris@0: use Symfony\Component\Debug\FatalErrorHandler\UndefinedMethodFatalErrorHandler; Chris@0: Chris@0: /** Chris@0: * A generic ErrorHandler for the PHP engine. Chris@0: * Chris@0: * Provides five bit fields that control how errors are handled: Chris@0: * - thrownErrors: errors thrown as \ErrorException Chris@0: * - loggedErrors: logged errors, when not @-silenced Chris@0: * - scopedErrors: errors thrown or logged with their local context Chris@0: * - tracedErrors: errors logged with their stack trace Chris@0: * - screamedErrors: never @-silenced errors Chris@0: * Chris@0: * Each error level can be logged by a dedicated PSR-3 logger object. Chris@0: * Screaming only applies to logging. Chris@0: * Throwing takes precedence over logging. Chris@0: * Uncaught exceptions are logged as E_ERROR. Chris@0: * E_DEPRECATED and E_USER_DEPRECATED levels never throw. Chris@0: * E_RECOVERABLE_ERROR and E_USER_ERROR levels always throw. Chris@0: * Non catchable errors that can be detected at shutdown time are logged when the scream bit field allows so. Chris@0: * As errors have a performance cost, repeated errors are all logged, so that the developer Chris@0: * can see them and weight them as more important to fix than others of the same level. Chris@0: * Chris@0: * @author Nicolas Grekas Chris@0: * @author Grégoire Pineau Chris@0: */ Chris@0: class ErrorHandler Chris@0: { Chris@17: private $levels = [ Chris@0: E_DEPRECATED => 'Deprecated', Chris@0: E_USER_DEPRECATED => 'User Deprecated', Chris@0: E_NOTICE => 'Notice', Chris@0: E_USER_NOTICE => 'User Notice', Chris@0: E_STRICT => 'Runtime Notice', Chris@0: E_WARNING => 'Warning', Chris@0: E_USER_WARNING => 'User Warning', Chris@0: E_COMPILE_WARNING => 'Compile Warning', Chris@0: E_CORE_WARNING => 'Core Warning', Chris@0: E_USER_ERROR => 'User Error', Chris@0: E_RECOVERABLE_ERROR => 'Catchable Fatal Error', Chris@0: E_COMPILE_ERROR => 'Compile Error', Chris@0: E_PARSE => 'Parse Error', Chris@0: E_ERROR => 'Error', Chris@0: E_CORE_ERROR => 'Core Error', Chris@17: ]; Chris@0: Chris@17: private $loggers = [ Chris@17: E_DEPRECATED => [null, LogLevel::INFO], Chris@17: E_USER_DEPRECATED => [null, LogLevel::INFO], Chris@17: E_NOTICE => [null, LogLevel::WARNING], Chris@17: E_USER_NOTICE => [null, LogLevel::WARNING], Chris@17: E_STRICT => [null, LogLevel::WARNING], Chris@17: E_WARNING => [null, LogLevel::WARNING], Chris@17: E_USER_WARNING => [null, LogLevel::WARNING], Chris@17: E_COMPILE_WARNING => [null, LogLevel::WARNING], Chris@17: E_CORE_WARNING => [null, LogLevel::WARNING], Chris@17: E_USER_ERROR => [null, LogLevel::CRITICAL], Chris@17: E_RECOVERABLE_ERROR => [null, LogLevel::CRITICAL], Chris@17: E_COMPILE_ERROR => [null, LogLevel::CRITICAL], Chris@17: E_PARSE => [null, LogLevel::CRITICAL], Chris@17: E_ERROR => [null, LogLevel::CRITICAL], Chris@17: E_CORE_ERROR => [null, LogLevel::CRITICAL], Chris@17: ]; Chris@0: Chris@0: private $thrownErrors = 0x1FFF; // E_ALL - E_DEPRECATED - E_USER_DEPRECATED Chris@0: private $scopedErrors = 0x1FFF; // E_ALL - E_DEPRECATED - E_USER_DEPRECATED Chris@0: private $tracedErrors = 0x77FB; // E_ALL - E_STRICT - E_PARSE Chris@0: private $screamedErrors = 0x55; // E_ERROR + E_CORE_ERROR + E_COMPILE_ERROR + E_PARSE Chris@0: private $loggedErrors = 0; Chris@0: private $traceReflector; Chris@0: Chris@0: private $isRecursive = 0; Chris@0: private $isRoot = false; Chris@0: private $exceptionHandler; Chris@0: private $bootstrappingLogger; Chris@0: Chris@0: private static $reservedMemory; Chris@17: private static $stackedErrors = []; Chris@17: private static $stackedErrorLevels = []; Chris@0: private static $toStringException = null; Chris@17: private static $silencedErrorCache = []; Chris@12: private static $silencedErrorCount = 0; Chris@0: private static $exitCode = 0; Chris@0: Chris@0: /** Chris@0: * Registers the error handler. Chris@0: * Chris@0: * @param self|null $handler The handler to register Chris@0: * @param bool $replace Whether to replace or not any existing handler Chris@0: * Chris@0: * @return self The registered error handler Chris@0: */ Chris@0: public static function register(self $handler = null, $replace = true) Chris@0: { Chris@0: if (null === self::$reservedMemory) { Chris@0: self::$reservedMemory = str_repeat('x', 10240); Chris@0: register_shutdown_function(__CLASS__.'::handleFatalError'); Chris@0: } Chris@0: Chris@0: if ($handlerIsNew = null === $handler) { Chris@0: $handler = new static(); Chris@0: } Chris@0: Chris@17: if (null === $prev = set_error_handler([$handler, 'handleError'])) { Chris@0: restore_error_handler(); Chris@0: // Specifying the error types earlier would expose us to https://bugs.php.net/63206 Chris@17: set_error_handler([$handler, 'handleError'], $handler->thrownErrors | $handler->loggedErrors); Chris@0: $handler->isRoot = true; Chris@0: } Chris@0: Chris@17: if ($handlerIsNew && \is_array($prev) && $prev[0] instanceof self) { Chris@0: $handler = $prev[0]; Chris@0: $replace = false; Chris@0: } Chris@12: if (!$replace && $prev) { Chris@12: restore_error_handler(); Chris@17: $handlerIsRegistered = \is_array($prev) && $handler === $prev[0]; Chris@13: } else { Chris@13: $handlerIsRegistered = true; Chris@12: } Chris@17: if (\is_array($prev = set_exception_handler([$handler, 'handleException'])) && $prev[0] instanceof self) { Chris@12: restore_exception_handler(); Chris@13: if (!$handlerIsRegistered) { Chris@13: $handler = $prev[0]; Chris@13: } elseif ($handler !== $prev[0] && $replace) { Chris@17: set_exception_handler([$handler, 'handleException']); Chris@13: $p = $prev[0]->setExceptionHandler(null); Chris@13: $handler->setExceptionHandler($p); Chris@13: $prev[0]->setExceptionHandler($p); Chris@13: } Chris@0: } else { Chris@12: $handler->setExceptionHandler($prev); Chris@0: } Chris@0: Chris@0: $handler->throwAt(E_ALL & $handler->thrownErrors, true); Chris@0: Chris@0: return $handler; Chris@0: } Chris@0: Chris@0: public function __construct(BufferingLogger $bootstrappingLogger = null) Chris@0: { Chris@0: if ($bootstrappingLogger) { Chris@0: $this->bootstrappingLogger = $bootstrappingLogger; Chris@0: $this->setDefaultLogger($bootstrappingLogger); Chris@0: } Chris@0: $this->traceReflector = new \ReflectionProperty('Exception', 'trace'); Chris@0: $this->traceReflector->setAccessible(true); Chris@0: } Chris@0: Chris@0: /** Chris@0: * Sets a logger to non assigned errors levels. Chris@0: * Chris@0: * @param LoggerInterface $logger A PSR-3 logger to put as default for the given levels Chris@0: * @param array|int $levels An array map of E_* to LogLevel::* or an integer bit field of E_* constants Chris@0: * @param bool $replace Whether to replace or not any existing logger Chris@0: */ Chris@0: public function setDefaultLogger(LoggerInterface $logger, $levels = E_ALL, $replace = false) Chris@0: { Chris@17: $loggers = []; Chris@0: Chris@17: if (\is_array($levels)) { Chris@0: foreach ($levels as $type => $logLevel) { Chris@0: if (empty($this->loggers[$type][0]) || $replace || $this->loggers[$type][0] === $this->bootstrappingLogger) { Chris@17: $loggers[$type] = [$logger, $logLevel]; Chris@0: } Chris@0: } Chris@0: } else { Chris@0: if (null === $levels) { Chris@0: $levels = E_ALL; Chris@0: } Chris@0: foreach ($this->loggers as $type => $log) { Chris@0: if (($type & $levels) && (empty($log[0]) || $replace || $log[0] === $this->bootstrappingLogger)) { Chris@0: $log[0] = $logger; Chris@0: $loggers[$type] = $log; Chris@0: } Chris@0: } Chris@0: } Chris@0: Chris@0: $this->setLoggers($loggers); Chris@0: } Chris@0: Chris@0: /** Chris@0: * Sets a logger for each error level. Chris@0: * Chris@0: * @param array $loggers Error levels to [LoggerInterface|null, LogLevel::*] map Chris@0: * Chris@0: * @return array The previous map Chris@0: * Chris@0: * @throws \InvalidArgumentException Chris@0: */ Chris@0: public function setLoggers(array $loggers) Chris@0: { Chris@0: $prevLogged = $this->loggedErrors; Chris@0: $prev = $this->loggers; Chris@17: $flush = []; Chris@0: Chris@0: foreach ($loggers as $type => $log) { Chris@0: if (!isset($prev[$type])) { Chris@0: throw new \InvalidArgumentException('Unknown error type: '.$type); Chris@0: } Chris@17: if (!\is_array($log)) { Chris@17: $log = [$log]; Chris@18: } elseif (!\array_key_exists(0, $log)) { Chris@0: throw new \InvalidArgumentException('No logger provided'); Chris@0: } Chris@0: if (null === $log[0]) { Chris@0: $this->loggedErrors &= ~$type; Chris@0: } elseif ($log[0] instanceof LoggerInterface) { Chris@0: $this->loggedErrors |= $type; Chris@0: } else { Chris@0: throw new \InvalidArgumentException('Invalid logger provided'); Chris@0: } Chris@0: $this->loggers[$type] = $log + $prev[$type]; Chris@0: Chris@0: if ($this->bootstrappingLogger && $prev[$type][0] === $this->bootstrappingLogger) { Chris@0: $flush[$type] = $type; Chris@0: } Chris@0: } Chris@0: $this->reRegister($prevLogged | $this->thrownErrors); Chris@0: Chris@0: if ($flush) { Chris@0: foreach ($this->bootstrappingLogger->cleanLogs() as $log) { Chris@0: $type = $log[2]['exception'] instanceof \ErrorException ? $log[2]['exception']->getSeverity() : E_ERROR; Chris@0: if (!isset($flush[$type])) { Chris@0: $this->bootstrappingLogger->log($log[0], $log[1], $log[2]); Chris@0: } elseif ($this->loggers[$type][0]) { Chris@0: $this->loggers[$type][0]->log($this->loggers[$type][1], $log[1], $log[2]); Chris@0: } Chris@0: } Chris@0: } Chris@0: Chris@0: return $prev; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Sets a user exception handler. Chris@0: * Chris@0: * @param callable $handler A handler that will be called on Exception Chris@0: * Chris@0: * @return callable|null The previous exception handler Chris@0: */ Chris@0: public function setExceptionHandler(callable $handler = null) Chris@0: { Chris@0: $prev = $this->exceptionHandler; Chris@0: $this->exceptionHandler = $handler; Chris@0: Chris@0: return $prev; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Sets the PHP error levels that throw an exception when a PHP error occurs. Chris@0: * Chris@0: * @param int $levels A bit field of E_* constants for thrown errors Chris@0: * @param bool $replace Replace or amend the previous value Chris@0: * Chris@0: * @return int The previous value Chris@0: */ Chris@0: public function throwAt($levels, $replace = false) Chris@0: { Chris@0: $prev = $this->thrownErrors; Chris@0: $this->thrownErrors = ($levels | E_RECOVERABLE_ERROR | E_USER_ERROR) & ~E_USER_DEPRECATED & ~E_DEPRECATED; Chris@0: if (!$replace) { Chris@0: $this->thrownErrors |= $prev; Chris@0: } Chris@0: $this->reRegister($prev | $this->loggedErrors); Chris@0: Chris@0: return $prev; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Sets the PHP error levels for which local variables are preserved. Chris@0: * Chris@0: * @param int $levels A bit field of E_* constants for scoped errors Chris@0: * @param bool $replace Replace or amend the previous value Chris@0: * Chris@0: * @return int The previous value Chris@0: */ Chris@0: public function scopeAt($levels, $replace = false) Chris@0: { Chris@0: $prev = $this->scopedErrors; Chris@0: $this->scopedErrors = (int) $levels; Chris@0: if (!$replace) { Chris@0: $this->scopedErrors |= $prev; Chris@0: } Chris@0: Chris@0: return $prev; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Sets the PHP error levels for which the stack trace is preserved. Chris@0: * Chris@0: * @param int $levels A bit field of E_* constants for traced errors Chris@0: * @param bool $replace Replace or amend the previous value Chris@0: * Chris@0: * @return int The previous value Chris@0: */ Chris@0: public function traceAt($levels, $replace = false) Chris@0: { Chris@0: $prev = $this->tracedErrors; Chris@0: $this->tracedErrors = (int) $levels; Chris@0: if (!$replace) { Chris@0: $this->tracedErrors |= $prev; Chris@0: } Chris@0: Chris@0: return $prev; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Sets the error levels where the @-operator is ignored. Chris@0: * Chris@0: * @param int $levels A bit field of E_* constants for screamed errors Chris@0: * @param bool $replace Replace or amend the previous value Chris@0: * Chris@0: * @return int The previous value Chris@0: */ Chris@0: public function screamAt($levels, $replace = false) Chris@0: { Chris@0: $prev = $this->screamedErrors; Chris@0: $this->screamedErrors = (int) $levels; Chris@0: if (!$replace) { Chris@0: $this->screamedErrors |= $prev; Chris@0: } Chris@0: Chris@0: return $prev; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Re-registers as a PHP error handler if levels changed. Chris@0: */ Chris@0: private function reRegister($prev) Chris@0: { Chris@0: if ($prev !== $this->thrownErrors | $this->loggedErrors) { Chris@0: $handler = set_error_handler('var_dump'); Chris@17: $handler = \is_array($handler) ? $handler[0] : null; Chris@0: restore_error_handler(); Chris@0: if ($handler === $this) { Chris@0: restore_error_handler(); Chris@0: if ($this->isRoot) { Chris@17: set_error_handler([$this, 'handleError'], $this->thrownErrors | $this->loggedErrors); Chris@0: } else { Chris@17: set_error_handler([$this, 'handleError']); Chris@0: } Chris@0: } Chris@0: } Chris@0: } Chris@0: Chris@0: /** Chris@0: * Handles errors by filtering then logging them according to the configured bit fields. Chris@0: * Chris@0: * @param int $type One of the E_* constants Chris@0: * @param string $message Chris@0: * @param string $file Chris@0: * @param int $line Chris@0: * Chris@0: * @return bool Returns false when no handling happens so that the PHP engine can handle the error itself Chris@0: * Chris@0: * @throws \ErrorException When $this->thrownErrors requests so Chris@0: * Chris@0: * @internal Chris@0: */ Chris@0: public function handleError($type, $message, $file, $line) Chris@0: { Chris@0: // Level is the current error reporting level to manage silent error. Chris@16: $level = error_reporting(); Chris@16: $silenced = 0 === ($level & $type); Chris@0: // Strong errors are not authorized to be silenced. Chris@16: $level |= E_RECOVERABLE_ERROR | E_USER_ERROR | E_DEPRECATED | E_USER_DEPRECATED; Chris@0: $log = $this->loggedErrors & $type; Chris@0: $throw = $this->thrownErrors & $type & $level; Chris@0: $type &= $level | $this->screamedErrors; Chris@0: Chris@0: if (!$type || (!$log && !$throw)) { Chris@16: return !$silenced && $type && $log; Chris@0: } Chris@0: $scope = $this->scopedErrors & $type; Chris@0: Chris@17: if (4 < $numArgs = \func_num_args()) { Chris@17: $context = $scope ? (func_get_arg(4) ?: []) : []; Chris@0: $backtrace = 5 < $numArgs ? func_get_arg(5) : null; // defined on HHVM Chris@0: } else { Chris@17: $context = []; Chris@0: $backtrace = null; Chris@0: } Chris@0: Chris@0: if (isset($context['GLOBALS']) && $scope) { Chris@0: $e = $context; // Whatever the signature of the method, Chris@0: unset($e['GLOBALS'], $context); // $context is always a reference in 5.3 Chris@0: $context = $e; Chris@0: } Chris@0: Chris@0: if (null !== $backtrace && $type & E_ERROR) { Chris@0: // E_ERROR fatal errors are triggered on HHVM when Chris@0: // hhvm.error_handling.call_user_handler_on_fatals=1 Chris@0: // which is the way to get their backtrace. Chris@0: $this->handleFatalError(compact('type', 'message', 'file', 'line', 'backtrace')); Chris@0: Chris@0: return true; Chris@0: } Chris@0: Chris@0: $logMessage = $this->levels[$type].': '.$message; Chris@0: Chris@0: if (null !== self::$toStringException) { Chris@0: $errorAsException = self::$toStringException; Chris@0: self::$toStringException = null; Chris@0: } elseif (!$throw && !($type & $level)) { Chris@12: if (!isset(self::$silencedErrorCache[$id = $file.':'.$line])) { Chris@17: $lightTrace = $this->tracedErrors & $type ? $this->cleanTrace(debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 3), $type, $file, $line, false) : []; Chris@12: $errorAsException = new SilencedErrorContext($type, $file, $line, $lightTrace); Chris@12: } elseif (isset(self::$silencedErrorCache[$id][$message])) { Chris@12: $lightTrace = null; Chris@12: $errorAsException = self::$silencedErrorCache[$id][$message]; Chris@12: ++$errorAsException->count; Chris@12: } else { Chris@17: $lightTrace = []; Chris@12: $errorAsException = null; Chris@12: } Chris@12: Chris@12: if (100 < ++self::$silencedErrorCount) { Chris@17: self::$silencedErrorCache = $lightTrace = []; Chris@12: self::$silencedErrorCount = 1; Chris@12: } Chris@12: if ($errorAsException) { Chris@12: self::$silencedErrorCache[$id][$message] = $errorAsException; Chris@12: } Chris@12: if (null === $lightTrace) { Chris@12: return; Chris@12: } Chris@0: } else { Chris@0: if ($scope) { Chris@0: $errorAsException = new ContextErrorException($logMessage, 0, $type, $file, $line, $context); Chris@0: } else { Chris@0: $errorAsException = new \ErrorException($logMessage, 0, $type, $file, $line); Chris@0: } Chris@0: Chris@0: // Clean the trace by removing function arguments and the first frames added by the error handler itself. Chris@0: if ($throw || $this->tracedErrors & $type) { Chris@0: $backtrace = $backtrace ?: $errorAsException->getTrace(); Chris@12: $lightTrace = $this->cleanTrace($backtrace, $type, $file, $line, $throw); Chris@0: $this->traceReflector->setValue($errorAsException, $lightTrace); Chris@0: } else { Chris@17: $this->traceReflector->setValue($errorAsException, []); Chris@0: } Chris@0: } Chris@0: Chris@0: if ($throw) { Chris@0: if (E_USER_ERROR & $type) { Chris@0: for ($i = 1; isset($backtrace[$i]); ++$i) { Chris@0: if (isset($backtrace[$i]['function'], $backtrace[$i]['type'], $backtrace[$i - 1]['function']) Chris@0: && '__toString' === $backtrace[$i]['function'] Chris@0: && '->' === $backtrace[$i]['type'] Chris@0: && !isset($backtrace[$i - 1]['class']) Chris@0: && ('trigger_error' === $backtrace[$i - 1]['function'] || 'user_error' === $backtrace[$i - 1]['function']) Chris@0: ) { Chris@0: // Here, we know trigger_error() has been called from __toString(). Chris@0: // HHVM is fine with throwing from __toString() but PHP triggers a fatal error instead. Chris@0: // A small convention allows working around the limitation: Chris@0: // given a caught $e exception in __toString(), quitting the method with Chris@0: // `return trigger_error($e, E_USER_ERROR);` allows this error handler Chris@0: // to make $e get through the __toString() barrier. Chris@0: Chris@0: foreach ($context as $e) { Chris@0: if (($e instanceof \Exception || $e instanceof \Throwable) && $e->__toString() === $message) { Chris@0: if (1 === $i) { Chris@0: // On HHVM Chris@0: $errorAsException = $e; Chris@0: break; Chris@0: } Chris@0: self::$toStringException = $e; Chris@0: Chris@0: return true; Chris@0: } Chris@0: } Chris@0: Chris@0: if (1 < $i) { Chris@0: // On PHP (not on HHVM), display the original error message instead of the default one. Chris@0: $this->handleException($errorAsException); Chris@0: Chris@0: // Stop the process by giving back the error to the native handler. Chris@0: return false; Chris@0: } Chris@0: } Chris@0: } Chris@0: } Chris@0: Chris@0: throw $errorAsException; Chris@0: } Chris@0: Chris@0: if ($this->isRecursive) { Chris@0: $log = 0; Chris@0: } elseif (self::$stackedErrorLevels) { Chris@17: self::$stackedErrors[] = [ Chris@0: $this->loggers[$type][0], Chris@0: ($type & $level) ? $this->loggers[$type][1] : LogLevel::DEBUG, Chris@0: $logMessage, Chris@17: $errorAsException ? ['exception' => $errorAsException] : [], Chris@17: ]; Chris@0: } else { Chris@18: if (!\defined('HHVM_VERSION')) { Chris@18: $currentErrorHandler = set_error_handler('var_dump'); Chris@18: restore_error_handler(); Chris@18: } Chris@18: Chris@0: try { Chris@0: $this->isRecursive = true; Chris@0: $level = ($type & $level) ? $this->loggers[$type][1] : LogLevel::DEBUG; Chris@17: $this->loggers[$type][0]->log($level, $logMessage, $errorAsException ? ['exception' => $errorAsException] : []); Chris@0: } finally { Chris@0: $this->isRecursive = false; Chris@17: Chris@17: if (!\defined('HHVM_VERSION')) { Chris@18: set_error_handler($currentErrorHandler); Chris@17: } Chris@0: } Chris@0: } Chris@0: Chris@16: return !$silenced && $type && $log; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Handles an exception by logging then forwarding it to another handler. Chris@0: * Chris@0: * @param \Exception|\Throwable $exception An exception to handle Chris@0: * @param array $error An array as returned by error_get_last() Chris@0: * Chris@0: * @internal Chris@0: */ Chris@0: public function handleException($exception, array $error = null) Chris@0: { Chris@0: if (null === $error) { Chris@0: self::$exitCode = 255; Chris@0: } Chris@0: if (!$exception instanceof \Exception) { Chris@0: $exception = new FatalThrowableError($exception); Chris@0: } Chris@0: $type = $exception instanceof FatalErrorException ? $exception->getSeverity() : E_ERROR; Chris@12: $handlerException = null; Chris@0: Chris@0: if (($this->loggedErrors & $type) || $exception instanceof FatalThrowableError) { Chris@0: if ($exception instanceof FatalErrorException) { Chris@0: if ($exception instanceof FatalThrowableError) { Chris@17: $error = [ Chris@0: 'type' => $type, Chris@0: 'message' => $message = $exception->getMessage(), Chris@0: 'file' => $exception->getFile(), Chris@0: 'line' => $exception->getLine(), Chris@17: ]; Chris@0: } else { Chris@0: $message = 'Fatal '.$exception->getMessage(); Chris@0: } Chris@0: } elseif ($exception instanceof \ErrorException) { Chris@0: $message = 'Uncaught '.$exception->getMessage(); Chris@0: } else { Chris@0: $message = 'Uncaught Exception: '.$exception->getMessage(); Chris@0: } Chris@0: } Chris@0: if ($this->loggedErrors & $type) { Chris@0: try { Chris@17: $this->loggers[$type][0]->log($this->loggers[$type][1], $message, ['exception' => $exception]); Chris@0: } catch (\Exception $handlerException) { Chris@0: } catch (\Throwable $handlerException) { Chris@0: } Chris@0: } Chris@0: if ($exception instanceof FatalErrorException && !$exception instanceof OutOfMemoryException && $error) { Chris@0: foreach ($this->getFatalErrorHandlers() as $handler) { Chris@0: if ($e = $handler->handleError($error, $exception)) { Chris@0: $exception = $e; Chris@0: break; Chris@0: } Chris@0: } Chris@0: } Chris@13: $exceptionHandler = $this->exceptionHandler; Chris@13: $this->exceptionHandler = null; Chris@0: try { Chris@13: if (null !== $exceptionHandler) { Chris@13: return \call_user_func($exceptionHandler, $exception); Chris@12: } Chris@12: $handlerException = $handlerException ?: $exception; Chris@0: } catch (\Exception $handlerException) { Chris@0: } catch (\Throwable $handlerException) { Chris@0: } Chris@12: if ($exception === $handlerException) { Chris@12: self::$reservedMemory = null; // Disable the fatal error handler Chris@12: throw $exception; // Give back $exception to the native handler Chris@0: } Chris@12: $this->handleException($handlerException); Chris@0: } Chris@0: Chris@0: /** Chris@0: * Shutdown registered function for handling PHP fatal errors. Chris@0: * Chris@0: * @param array $error An array as returned by error_get_last() Chris@0: * Chris@0: * @internal Chris@0: */ Chris@0: public static function handleFatalError(array $error = null) Chris@0: { Chris@0: if (null === self::$reservedMemory) { Chris@0: return; Chris@0: } Chris@0: Chris@12: $handler = self::$reservedMemory = null; Chris@17: $handlers = []; Chris@12: $previousHandler = null; Chris@12: $sameHandlerLimit = 10; Chris@0: Chris@17: while (!\is_array($handler) || !$handler[0] instanceof self) { Chris@12: $handler = set_exception_handler('var_dump'); Chris@12: restore_exception_handler(); Chris@0: Chris@12: if (!$handler) { Chris@12: break; Chris@12: } Chris@12: restore_exception_handler(); Chris@12: Chris@12: if ($handler !== $previousHandler) { Chris@12: array_unshift($handlers, $handler); Chris@12: $previousHandler = $handler; Chris@12: } elseif (0 === --$sameHandlerLimit) { Chris@12: $handler = null; Chris@12: break; Chris@12: } Chris@12: } Chris@12: foreach ($handlers as $h) { Chris@12: set_exception_handler($h); Chris@12: } Chris@12: if (!$handler) { Chris@0: return; Chris@0: } Chris@12: if ($handler !== $h) { Chris@12: $handler[0]->setExceptionHandler($h); Chris@12: } Chris@12: $handler = $handler[0]; Chris@17: $handlers = []; Chris@0: Chris@0: if ($exit = null === $error) { Chris@0: $error = error_get_last(); Chris@0: } Chris@0: Chris@0: try { Chris@0: while (self::$stackedErrorLevels) { Chris@0: static::unstackErrors(); Chris@0: } Chris@0: } catch (\Exception $exception) { Chris@0: // Handled below Chris@0: } catch (\Throwable $exception) { Chris@0: // Handled below Chris@0: } Chris@0: Chris@0: if ($error && $error['type'] &= E_PARSE | E_ERROR | E_CORE_ERROR | E_COMPILE_ERROR) { Chris@0: // Let's not throw anymore but keep logging Chris@0: $handler->throwAt(0, true); Chris@0: $trace = isset($error['backtrace']) ? $error['backtrace'] : null; Chris@0: Chris@0: if (0 === strpos($error['message'], 'Allowed memory') || 0 === strpos($error['message'], 'Out of memory')) { Chris@0: $exception = new OutOfMemoryException($handler->levels[$error['type']].': '.$error['message'], 0, $error['type'], $error['file'], $error['line'], 2, false, $trace); Chris@0: } else { Chris@0: $exception = new FatalErrorException($handler->levels[$error['type']].': '.$error['message'], 0, $error['type'], $error['file'], $error['line'], 2, true, $trace); Chris@0: } Chris@0: } Chris@0: Chris@0: try { Chris@0: if (isset($exception)) { Chris@0: self::$exitCode = 255; Chris@0: $handler->handleException($exception, $error); Chris@0: } Chris@0: } catch (FatalErrorException $e) { Chris@0: // Ignore this re-throw Chris@0: } Chris@0: Chris@0: if ($exit && self::$exitCode) { Chris@0: $exitCode = self::$exitCode; Chris@0: register_shutdown_function('register_shutdown_function', function () use ($exitCode) { exit($exitCode); }); Chris@0: } Chris@0: } Chris@0: Chris@0: /** Chris@0: * Configures the error handler for delayed handling. Chris@0: * Ensures also that non-catchable fatal errors are never silenced. Chris@0: * Chris@0: * As shown by http://bugs.php.net/42098 and http://bugs.php.net/60724 Chris@0: * PHP has a compile stage where it behaves unusually. To workaround it, Chris@0: * we plug an error handler that only stacks errors for later. Chris@0: * Chris@0: * The most important feature of this is to prevent Chris@0: * autoloading until unstackErrors() is called. Chris@12: * Chris@12: * @deprecated since version 3.4, to be removed in 4.0. Chris@0: */ Chris@0: public static function stackErrors() Chris@0: { Chris@12: @trigger_error('Support for stacking errors is deprecated since Symfony 3.4 and will be removed in 4.0.', E_USER_DEPRECATED); Chris@12: Chris@0: self::$stackedErrorLevels[] = error_reporting(error_reporting() | E_PARSE | E_ERROR | E_CORE_ERROR | E_COMPILE_ERROR); Chris@0: } Chris@0: Chris@0: /** Chris@0: * Unstacks stacked errors and forwards to the logger. Chris@12: * Chris@12: * @deprecated since version 3.4, to be removed in 4.0. Chris@0: */ Chris@0: public static function unstackErrors() Chris@0: { Chris@12: @trigger_error('Support for unstacking errors is deprecated since Symfony 3.4 and will be removed in 4.0.', E_USER_DEPRECATED); Chris@12: Chris@0: $level = array_pop(self::$stackedErrorLevels); Chris@0: Chris@0: if (null !== $level) { Chris@0: $errorReportingLevel = error_reporting($level); Chris@0: if ($errorReportingLevel !== ($level | E_PARSE | E_ERROR | E_CORE_ERROR | E_COMPILE_ERROR)) { Chris@0: // If the user changed the error level, do not overwrite it Chris@0: error_reporting($errorReportingLevel); Chris@0: } Chris@0: } Chris@0: Chris@0: if (empty(self::$stackedErrorLevels)) { Chris@0: $errors = self::$stackedErrors; Chris@17: self::$stackedErrors = []; Chris@0: Chris@0: foreach ($errors as $error) { Chris@0: $error[0]->log($error[1], $error[2], $error[3]); Chris@0: } Chris@0: } Chris@0: } Chris@0: Chris@0: /** Chris@0: * Gets the fatal error handlers. Chris@0: * Chris@0: * Override this method if you want to define more fatal error handlers. Chris@0: * Chris@0: * @return FatalErrorHandlerInterface[] An array of FatalErrorHandlerInterface Chris@0: */ Chris@0: protected function getFatalErrorHandlers() Chris@0: { Chris@17: return [ Chris@0: new UndefinedFunctionFatalErrorHandler(), Chris@0: new UndefinedMethodFatalErrorHandler(), Chris@0: new ClassNotFoundFatalErrorHandler(), Chris@17: ]; Chris@0: } Chris@12: Chris@12: private function cleanTrace($backtrace, $type, $file, $line, $throw) Chris@12: { Chris@12: $lightTrace = $backtrace; Chris@12: Chris@12: for ($i = 0; isset($backtrace[$i]); ++$i) { Chris@12: if (isset($backtrace[$i]['file'], $backtrace[$i]['line']) && $backtrace[$i]['line'] === $line && $backtrace[$i]['file'] === $file) { Chris@17: $lightTrace = \array_slice($lightTrace, 1 + $i); Chris@12: break; Chris@12: } Chris@12: } Chris@12: if (!($throw || $this->scopedErrors & $type)) { Chris@12: for ($i = 0; isset($lightTrace[$i]); ++$i) { Chris@12: unset($lightTrace[$i]['args'], $lightTrace[$i]['object']); Chris@12: } Chris@12: } Chris@12: Chris@12: return $lightTrace; Chris@12: } Chris@0: }