diff --git a/src/Symfony/Bundle/FrameworkBundle/Console/Application.php b/src/Symfony/Bundle/FrameworkBundle/Console/Application.php index 0535d26998..319b9be9dd 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Console/Application.php +++ b/src/Symfony/Bundle/FrameworkBundle/Console/Application.php @@ -19,8 +19,8 @@ use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\ConsoleOutputInterface; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Style\SymfonyStyle; +use Symfony\Component\Debug\Exception\FatalThrowableError; use Symfony\Component\DependencyInjection\ContainerAwareInterface; -use Symfony\Component\ErrorCatcher\Exception\FatalThrowableError; use Symfony\Component\HttpKernel\Bundle\Bundle; use Symfony\Component\HttpKernel\Kernel; use Symfony\Component\HttpKernel\KernelInterface; diff --git a/src/Symfony/Bundle/FrameworkBundle/FrameworkBundle.php b/src/Symfony/Bundle/FrameworkBundle/FrameworkBundle.php index 6ee99382ea..0ef349ddb5 100644 --- a/src/Symfony/Bundle/FrameworkBundle/FrameworkBundle.php +++ b/src/Symfony/Bundle/FrameworkBundle/FrameworkBundle.php @@ -29,11 +29,11 @@ use Symfony\Component\Cache\DependencyInjection\CachePoolPass; use Symfony\Component\Cache\DependencyInjection\CachePoolPrunerPass; use Symfony\Component\Config\Resource\ClassExistenceResource; use Symfony\Component\Console\DependencyInjection\AddConsoleCommandPass; +use Symfony\Component\Debug\ErrorHandler; use Symfony\Component\DependencyInjection\Compiler\PassConfig; use Symfony\Component\DependencyInjection\Compiler\RegisterReverseContainerPass; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\ErrorCatcher\DependencyInjection\ErrorCatcherPass; -use Symfony\Component\ErrorCatcher\ErrorHandler; use Symfony\Component\EventDispatcher\DependencyInjection\RegisterListenersPass; use Symfony\Component\Form\DependencyInjection\FormPass; use Symfony\Component\HttpFoundation\Request; diff --git a/src/Symfony/Component/Console/Application.php b/src/Symfony/Component/Console/Application.php index 571ab7cb2f..10ae964d8c 100644 --- a/src/Symfony/Component/Console/Application.php +++ b/src/Symfony/Component/Console/Application.php @@ -41,8 +41,8 @@ use Symfony\Component\Console\Output\ConsoleOutput; use Symfony\Component\Console\Output\ConsoleOutputInterface; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Style\SymfonyStyle; -use Symfony\Component\ErrorCatcher\ErrorHandler; -use Symfony\Component\ErrorCatcher\Exception\FatalThrowableError; +use Symfony\Component\Debug\ErrorHandler; +use Symfony\Component\Debug\Exception\FatalThrowableError; use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Symfony\Component\EventDispatcher\LegacyEventDispatcherProxy; use Symfony\Contracts\Service\ResetInterface; diff --git a/src/Symfony/Component/Debug/BufferingLogger.php b/src/Symfony/Component/Debug/BufferingLogger.php index 6f801c0ab3..e7db3a4ce4 100644 --- a/src/Symfony/Component/Debug/BufferingLogger.php +++ b/src/Symfony/Component/Debug/BufferingLogger.php @@ -11,16 +11,12 @@ namespace Symfony\Component\Debug; -@trigger_error(sprintf('The "%s" class is deprecated since Symfony 4.4 and will be removed in 5.0.', BufferingLogger::class), E_USER_DEPRECATED); - use Psr\Log\AbstractLogger; /** * A buffering logger that stacks logs for later. * * @author Nicolas Grekas - * - * @deprecated since Symfony 4.4 and will be removed in 5.0. */ class BufferingLogger extends AbstractLogger { diff --git a/src/Symfony/Component/Debug/CHANGELOG.md b/src/Symfony/Component/Debug/CHANGELOG.md index 71da439940..e84aa18552 100644 --- a/src/Symfony/Component/Debug/CHANGELOG.md +++ b/src/Symfony/Component/Debug/CHANGELOG.md @@ -4,14 +4,7 @@ CHANGELOG 4.4.0 ----- -* deprecated the `BufferingLogger`, `ErrorHandler` and `ExceptionHandler` classes, - they have been moved to the `ErrorCatcher` component -* deprecated the `FatalErrorHandlerInterface`, `ClassNotFoundFatalErrorHandler`, - `UndefinedFunctionFatalErrorHandler` and `UndefinedMethodFatalErrorHandler` classes, - they have been moved to the `ErrorCatcher` component -* deprecated the `ClassNotFoundException`, `FatalErrorException`, `FatalThrowableError`, - `FlattenException`, `OutOfMemoryException`, `SilencedErrorContext`, `UndefinedFunctionException`, - and `UndefinedMethodException`, they have been moved to the `ErrorCatcher` component + * deprecated `FlattenException`, use the `FlattenException` of the `ErrorCatcher` component 4.3.0 ----- diff --git a/src/Symfony/Component/Debug/Debug.php b/src/Symfony/Component/Debug/Debug.php index 8757476085..5d2d55cf9f 100644 --- a/src/Symfony/Component/Debug/Debug.php +++ b/src/Symfony/Component/Debug/Debug.php @@ -11,10 +11,6 @@ namespace Symfony\Component\Debug; -use Symfony\Component\ErrorCatcher\BufferingLogger; -use Symfony\Component\ErrorCatcher\ErrorHandler; -use Symfony\Component\ErrorCatcher\ExceptionHandler; - /** * Registers all the debug tools. * diff --git a/src/Symfony/Component/Debug/DebugClassLoader.php b/src/Symfony/Component/Debug/DebugClassLoader.php index 94179f25a5..ff9a8d72f9 100644 --- a/src/Symfony/Component/Debug/DebugClassLoader.php +++ b/src/Symfony/Component/Debug/DebugClassLoader.php @@ -86,7 +86,7 @@ class DebugClassLoader public static function enable() { // Ensures we don't hit https://bugs.php.net/42098 - class_exists('Symfony\Component\ErrorCatcher\ErrorHandler'); + class_exists('Symfony\Component\Debug\ErrorHandler'); class_exists('Psr\Log\LogLevel'); if (!\is_array($functions = spl_autoload_functions())) { diff --git a/src/Symfony/Component/Debug/ErrorHandler.php b/src/Symfony/Component/Debug/ErrorHandler.php index 68c90c253d..a99a000b07 100644 --- a/src/Symfony/Component/Debug/ErrorHandler.php +++ b/src/Symfony/Component/Debug/ErrorHandler.php @@ -11,13 +11,705 @@ namespace Symfony\Component\Debug; -use Symfony\Component\ErrorCatcher\ErrorHandler as BaseErrorHandler; - -@trigger_error(sprintf('The "%s" class is deprecated since Symfony 4.4, use "%s" instead.', ErrorHandler::class, BaseErrorHandler::class), E_USER_DEPRECATED); +use Psr\Log\LoggerInterface; +use Psr\Log\LogLevel; +use Symfony\Component\Debug\Exception\FatalErrorException; +use Symfony\Component\Debug\Exception\FatalThrowableError; +use Symfony\Component\Debug\Exception\FlattenException; +use Symfony\Component\Debug\Exception\OutOfMemoryException; +use Symfony\Component\Debug\Exception\SilencedErrorContext; +use Symfony\Component\Debug\FatalErrorHandler\ClassNotFoundFatalErrorHandler; +use Symfony\Component\Debug\FatalErrorHandler\FatalErrorHandlerInterface; +use Symfony\Component\Debug\FatalErrorHandler\UndefinedFunctionFatalErrorHandler; +use Symfony\Component\Debug\FatalErrorHandler\UndefinedMethodFatalErrorHandler; /** - * @deprecated since Symfony 4.4, use Symfony\Component\ErrorCatcher\ErrorHandler instead. + * A generic ErrorHandler for the PHP engine. + * + * Provides five bit fields that control how errors are handled: + * - 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 + * - screamedErrors: never @-silenced errors + * + * Each error level can be logged by a dedicated PSR-3 logger object. + * Screaming only applies to logging. + * Throwing takes precedence over logging. + * Uncaught exceptions are logged as E_ERROR. + * E_DEPRECATED and E_USER_DEPRECATED levels never throw. + * E_RECOVERABLE_ERROR and E_USER_ERROR levels always throw. + * Non catchable errors that can be detected at shutdown time are logged when the scream bit field allows so. + * As errors have a performance cost, repeated errors are all logged, so that the developer + * can see them and weight them as more important to fix than others of the same level. + * + * @author Nicolas Grekas + * @author Grégoire Pineau + * + * @final since Symfony 4.3 */ -class ErrorHandler extends BaseErrorHandler +class ErrorHandler { + private $levels = [ + E_DEPRECATED => 'Deprecated', + E_USER_DEPRECATED => 'User Deprecated', + E_NOTICE => 'Notice', + E_USER_NOTICE => 'User Notice', + E_STRICT => 'Runtime Notice', + E_WARNING => 'Warning', + E_USER_WARNING => 'User Warning', + E_COMPILE_WARNING => 'Compile Warning', + E_CORE_WARNING => 'Core Warning', + E_USER_ERROR => 'User Error', + E_RECOVERABLE_ERROR => 'Catchable Fatal Error', + E_COMPILE_ERROR => 'Compile Error', + E_PARSE => 'Parse Error', + E_ERROR => 'Error', + E_CORE_ERROR => 'Core Error', + ]; + + private $loggers = [ + E_DEPRECATED => [null, LogLevel::INFO], + E_USER_DEPRECATED => [null, LogLevel::INFO], + E_NOTICE => [null, LogLevel::WARNING], + E_USER_NOTICE => [null, LogLevel::WARNING], + E_STRICT => [null, LogLevel::WARNING], + E_WARNING => [null, LogLevel::WARNING], + E_USER_WARNING => [null, LogLevel::WARNING], + E_COMPILE_WARNING => [null, LogLevel::WARNING], + E_CORE_WARNING => [null, LogLevel::WARNING], + E_USER_ERROR => [null, LogLevel::CRITICAL], + E_RECOVERABLE_ERROR => [null, LogLevel::CRITICAL], + E_COMPILE_ERROR => [null, LogLevel::CRITICAL], + E_PARSE => [null, LogLevel::CRITICAL], + E_ERROR => [null, LogLevel::CRITICAL], + E_CORE_ERROR => [null, LogLevel::CRITICAL], + ]; + + private $thrownErrors = 0x1FFF; // E_ALL - E_DEPRECATED - E_USER_DEPRECATED + private $scopedErrors = 0x1FFF; // E_ALL - E_DEPRECATED - E_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 $isRecursive = 0; + private $isRoot = false; + private $exceptionHandler; + private $bootstrappingLogger; + + private static $reservedMemory; + private static $toStringException = null; + private static $silencedErrorCache = []; + private static $silencedErrorCount = 0; + private static $exitCode = 0; + + /** + * Registers the error 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(self $handler = null, $replace = true) + { + if (null === self::$reservedMemory) { + self::$reservedMemory = str_repeat('x', 10240); + register_shutdown_function(__CLASS__.'::handleFatalError'); + } + + if ($handlerIsNew = null === $handler) { + $handler = new static(); + } + + if (null === $prev = set_error_handler([$handler, 'handleError'])) { + restore_error_handler(); + // Specifying the error types earlier would expose us to https://bugs.php.net/63206 + set_error_handler([$handler, 'handleError'], $handler->thrownErrors | $handler->loggedErrors); + $handler->isRoot = true; + } + + if ($handlerIsNew && \is_array($prev) && $prev[0] instanceof self) { + $handler = $prev[0]; + $replace = false; + } + if (!$replace && $prev) { + restore_error_handler(); + $handlerIsRegistered = \is_array($prev) && $handler === $prev[0]; + } else { + $handlerIsRegistered = true; + } + if (\is_array($prev = set_exception_handler([$handler, 'handleException'])) && $prev[0] instanceof self) { + restore_exception_handler(); + if (!$handlerIsRegistered) { + $handler = $prev[0]; + } elseif ($handler !== $prev[0] && $replace) { + set_exception_handler([$handler, 'handleException']); + $p = $prev[0]->setExceptionHandler(null); + $handler->setExceptionHandler($p); + $prev[0]->setExceptionHandler($p); + } + } else { + $handler->setExceptionHandler($prev); + } + + $handler->throwAt(E_ALL & $handler->thrownErrors, true); + + return $handler; + } + + public function __construct(BufferingLogger $bootstrappingLogger = null) + { + if ($bootstrappingLogger) { + $this->bootstrappingLogger = $bootstrappingLogger; + $this->setDefaultLogger($bootstrappingLogger); + } + $this->traceReflector = new \ReflectionProperty('Exception', 'trace'); + $this->traceReflector->setAccessible(true); + } + + /** + * Sets a logger to non assigned errors levels. + * + * @param LoggerInterface $logger A PSR-3 logger to put as default for the given levels + * @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 = E_ALL, $replace = false) + { + $loggers = []; + + if (\is_array($levels)) { + foreach ($levels as $type => $logLevel) { + if (empty($this->loggers[$type][0]) || $replace || $this->loggers[$type][0] === $this->bootstrappingLogger) { + $loggers[$type] = [$logger, $logLevel]; + } + } + } else { + if (null === $levels) { + $levels = E_ALL; + } + foreach ($this->loggers as $type => $log) { + if (($type & $levels) && (empty($log[0]) || $replace || $log[0] === $this->bootstrappingLogger)) { + $log[0] = $logger; + $loggers[$type] = $log; + } + } + } + + $this->setLoggers($loggers); + } + + /** + * Sets a logger for each error level. + * + * @param array $loggers Error levels to [LoggerInterface|null, LogLevel::*] map + * + * @return array The previous map + * + * @throws \InvalidArgumentException + */ + public function setLoggers(array $loggers) + { + $prevLogged = $this->loggedErrors; + $prev = $this->loggers; + $flush = []; + + foreach ($loggers as $type => $log) { + if (!isset($prev[$type])) { + throw new \InvalidArgumentException('Unknown error type: '.$type); + } + if (!\is_array($log)) { + $log = [$log]; + } elseif (!\array_key_exists(0, $log)) { + throw new \InvalidArgumentException('No logger provided'); + } + if (null === $log[0]) { + $this->loggedErrors &= ~$type; + } elseif ($log[0] instanceof LoggerInterface) { + $this->loggedErrors |= $type; + } else { + throw new \InvalidArgumentException('Invalid logger provided'); + } + $this->loggers[$type] = $log + $prev[$type]; + + if ($this->bootstrappingLogger && $prev[$type][0] === $this->bootstrappingLogger) { + $flush[$type] = $type; + } + } + $this->reRegister($prevLogged | $this->thrownErrors); + + if ($flush) { + foreach ($this->bootstrappingLogger->cleanLogs() as $log) { + $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]) { + $this->loggers[$type][0]->log($this->loggers[$type][1], $log[1], $log[2]); + } + } + } + + return $prev; + } + + /** + * Sets a user exception handler. + * + * @param callable $handler A handler that will be called on Exception + * + * @return callable|null The previous exception handler + */ + public function setExceptionHandler(callable $handler = null) + { + $prev = $this->exceptionHandler; + $this->exceptionHandler = $handler; + + return $prev; + } + + /** + * Sets the PHP error levels that throw an exception when a PHP error occurs. + * + * @param int $levels A bit field of E_* constants for thrown errors + * @param bool $replace Replace or amend the previous value + * + * @return int The previous value + */ + public function throwAt($levels, $replace = false) + { + $prev = $this->thrownErrors; + $this->thrownErrors = ($levels | E_RECOVERABLE_ERROR | E_USER_ERROR) & ~E_USER_DEPRECATED & ~E_DEPRECATED; + if (!$replace) { + $this->thrownErrors |= $prev; + } + $this->reRegister($prev | $this->loggedErrors); + + return $prev; + } + + /** + * Sets the PHP error levels for which local variables are preserved. + * + * @param int $levels A bit field of E_* constants for scoped errors + * @param bool $replace Replace or amend the previous value + * + * @return int The previous value + */ + public function scopeAt($levels, $replace = false) + { + $prev = $this->scopedErrors; + $this->scopedErrors = (int) $levels; + if (!$replace) { + $this->scopedErrors |= $prev; + } + + return $prev; + } + + /** + * Sets the PHP error levels for which the stack trace is preserved. + * + * @param int $levels A bit field of E_* constants for traced errors + * @param bool $replace Replace or amend the previous value + * + * @return int The previous value + */ + public function traceAt($levels, $replace = false) + { + $prev = $this->tracedErrors; + $this->tracedErrors = (int) $levels; + if (!$replace) { + $this->tracedErrors |= $prev; + } + + return $prev; + } + + /** + * Sets the error levels where the @-operator is ignored. + * + * @param int $levels A bit field of E_* constants for screamed errors + * @param bool $replace Replace or amend the previous value + * + * @return int The previous value + */ + public function screamAt($levels, $replace = false) + { + $prev = $this->screamedErrors; + $this->screamedErrors = (int) $levels; + if (!$replace) { + $this->screamedErrors |= $prev; + } + + return $prev; + } + + /** + * Re-registers as a PHP error handler if levels changed. + */ + private function reRegister($prev) + { + if ($prev !== $this->thrownErrors | $this->loggedErrors) { + $handler = set_error_handler('var_dump'); + $handler = \is_array($handler) ? $handler[0] : null; + restore_error_handler(); + if ($handler === $this) { + restore_error_handler(); + if ($this->isRoot) { + set_error_handler([$this, 'handleError'], $this->thrownErrors | $this->loggedErrors); + } else { + set_error_handler([$this, 'handleError']); + } + } + } + } + + /** + * Handles errors by filtering then logging them according to the configured bit fields. + * + * @param int $type One of the E_* constants + * @param string $message + * @param string $file + * @param int $line + * + * @return bool Returns false when no handling happens so that the PHP engine can handle the error itself + * + * @throws \ErrorException When $this->thrownErrors requests so + * + * @internal + */ + public function handleError($type, $message, $file, $line) + { + // @deprecated to be removed in Symfony 5.0 + if (\PHP_VERSION_ID >= 70300 && $message && '"' === $message[0] && 0 === strpos($message, '"continue') && preg_match('/^"continue(?: \d++)?" targeting switch is equivalent to "break(?: \d++)?"\. Did you mean to use "continue(?: \d++)?"\?$/', $message)) { + $type = E_DEPRECATED; + } + + // Level is the current error reporting level to manage silent error. + $level = error_reporting(); + $silenced = 0 === ($level & $type); + // Strong errors are not authorized to be silenced. + $level |= E_RECOVERABLE_ERROR | E_USER_ERROR | E_DEPRECATED | E_USER_DEPRECATED; + $log = $this->loggedErrors & $type; + $throw = $this->thrownErrors & $type & $level; + $type &= $level | $this->screamedErrors; + + if (!$type || (!$log && !$throw)) { + return !$silenced && $type && $log; + } + $scope = $this->scopedErrors & $type; + + if (4 < $numArgs = \func_num_args()) { + $context = $scope ? (func_get_arg(4) ?: []) : []; + } else { + $context = []; + } + + if (isset($context['GLOBALS']) && $scope) { + $e = $context; // Whatever the signature of the method, + unset($e['GLOBALS'], $context); // $context is always a reference in 5.3 + $context = $e; + } + + if (false !== strpos($message, "class@anonymous\0")) { + $logMessage = $this->levels[$type].': '.(new FlattenException())->setMessage($message)->getMessage(); + } else { + $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, 5), $type, $file, $line, false) : []; + $errorAsException = new SilencedErrorContext($type, $file, $line, isset($lightTrace[1]) ? [$lightTrace[0]] : $lightTrace); + } elseif (isset(self::$silencedErrorCache[$id][$message])) { + $lightTrace = null; + $errorAsException = self::$silencedErrorCache[$id][$message]; + ++$errorAsException->count; + } else { + $lightTrace = []; + $errorAsException = null; + } + + if (100 < ++self::$silencedErrorCount) { + self::$silencedErrorCache = $lightTrace = []; + self::$silencedErrorCount = 1; + } + if ($errorAsException) { + self::$silencedErrorCache[$id][$message] = $errorAsException; + } + if (null === $lightTrace) { + return; + } + } else { + $errorAsException = new \ErrorException($logMessage, 0, $type, $file, $line); + + if ($throw || $this->tracedErrors & $type) { + $backtrace = $errorAsException->getTrace(); + $lightTrace = $this->cleanTrace($backtrace, $type, $file, $line, $throw); + $this->traceReflector->setValue($errorAsException, $lightTrace); + } else { + $this->traceReflector->setValue($errorAsException, []); + $backtrace = []; + } + } + + if ($throw) { + if (E_USER_ERROR & $type) { + for ($i = 1; isset($backtrace[$i]); ++$i) { + if (isset($backtrace[$i]['function'], $backtrace[$i]['type'], $backtrace[$i - 1]['function']) + && '__toString' === $backtrace[$i]['function'] + && '->' === $backtrace[$i]['type'] + && !isset($backtrace[$i - 1]['class']) + && ('trigger_error' === $backtrace[$i - 1]['function'] || 'user_error' === $backtrace[$i - 1]['function']) + ) { + // Here, we know trigger_error() has been called from __toString(). + // PHP triggers a fatal error when throwing from __toString(). + // A small convention allows working around the limitation: + // given a caught $e exception in __toString(), quitting the method with + // `return trigger_error($e, E_USER_ERROR);` allows this error handler + // to make $e get through the __toString() barrier. + + foreach ($context as $e) { + if ($e instanceof \Throwable && $e->__toString() === $message) { + self::$toStringException = $e; + + return true; + } + } + + // Display the original error message instead of the default one. + $this->handleException($errorAsException); + + // Stop the process by giving back the error to the native handler. + return false; + } + } + } + + throw $errorAsException; + } + + if ($this->isRecursive) { + $log = 0; + } else { + if (!\defined('HHVM_VERSION')) { + $currentErrorHandler = set_error_handler('var_dump'); + restore_error_handler(); + } + + try { + $this->isRecursive = true; + $level = ($type & $level) ? $this->loggers[$type][1] : LogLevel::DEBUG; + $this->loggers[$type][0]->log($level, $logMessage, $errorAsException ? ['exception' => $errorAsException] : []); + } finally { + $this->isRecursive = false; + + if (!\defined('HHVM_VERSION')) { + set_error_handler($currentErrorHandler); + } + } + } + + return !$silenced && $type && $log; + } + + /** + * Handles an exception by logging then forwarding it to another handler. + * + * @param \Exception|\Throwable $exception An exception to handle + * @param array $error An array as returned by error_get_last() + * + * @internal + */ + public function handleException($exception, array $error = null) + { + if (null === $error) { + self::$exitCode = 255; + } + if (!$exception instanceof \Exception) { + $exception = new FatalThrowableError($exception); + } + $type = $exception instanceof FatalErrorException ? $exception->getSeverity() : E_ERROR; + $handlerException = null; + + if (($this->loggedErrors & $type) || $exception instanceof FatalThrowableError) { + if (false !== strpos($message = $exception->getMessage(), "class@anonymous\0")) { + $message = (new FlattenException())->setMessage($message)->getMessage(); + } + if ($exception instanceof FatalErrorException) { + if ($exception instanceof FatalThrowableError) { + $error = [ + 'type' => $type, + 'message' => $message, + 'file' => $exception->getFile(), + 'line' => $exception->getLine(), + ]; + } else { + $message = 'Fatal '.$message; + } + } elseif ($exception instanceof \ErrorException) { + $message = 'Uncaught '.$message; + } else { + $message = 'Uncaught Exception: '.$message; + } + } + if ($this->loggedErrors & $type) { + try { + $this->loggers[$type][0]->log($this->loggers[$type][1], $message, ['exception' => $exception]); + } catch (\Throwable $handlerException) { + } + } + if ($exception instanceof FatalErrorException && !$exception instanceof OutOfMemoryException && $error) { + foreach ($this->getFatalErrorHandlers() as $handler) { + if ($e = $handler->handleError($error, $exception)) { + $exception = $e; + break; + } + } + } + $exceptionHandler = $this->exceptionHandler; + $this->exceptionHandler = null; + try { + if (null !== $exceptionHandler) { + return $exceptionHandler($exception); + } + $handlerException = $handlerException ?: $exception; + } catch (\Throwable $handlerException) { + } + if ($exception === $handlerException) { + self::$reservedMemory = null; // Disable the fatal error handler + throw $exception; // Give back $exception to the native handler + } + $this->handleException($handlerException); + } + + /** + * Shutdown registered function for handling PHP fatal errors. + * + * @param array $error An array as returned by error_get_last() + * + * @internal + */ + public static function handleFatalError(array $error = null) + { + if (null === self::$reservedMemory) { + return; + } + + $handler = self::$reservedMemory = null; + $handlers = []; + $previousHandler = null; + $sameHandlerLimit = 10; + + while (!\is_array($handler) || !$handler[0] instanceof self) { + $handler = set_exception_handler('var_dump'); + restore_exception_handler(); + + 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 = []; + + if ($exit = null === $error) { + $error = error_get_last(); + } + + if ($error && $error['type'] &= E_PARSE | E_ERROR | E_CORE_ERROR | E_COMPILE_ERROR) { + // Let's not throw anymore but keep logging + $handler->throwAt(0, true); + $trace = isset($error['backtrace']) ? $error['backtrace'] : null; + + if (0 === strpos($error['message'], 'Allowed memory') || 0 === strpos($error['message'], 'Out of memory')) { + $exception = new OutOfMemoryException($handler->levels[$error['type']].': '.$error['message'], 0, $error['type'], $error['file'], $error['line'], 2, false, $trace); + } else { + $exception = new FatalErrorException($handler->levels[$error['type']].': '.$error['message'], 0, $error['type'], $error['file'], $error['line'], 2, true, $trace); + } + } else { + $exception = null; + } + + try { + if (null !== $exception) { + self::$exitCode = 255; + $handler->handleException($exception, $error); + } + } catch (FatalErrorException $e) { + // Ignore this re-throw + } + + if ($exit && self::$exitCode) { + $exitCode = self::$exitCode; + register_shutdown_function('register_shutdown_function', function () use ($exitCode) { exit($exitCode); }); + } + } + + /** + * Gets the fatal error handlers. + * + * Override this method if you want to define more fatal error handlers. + * + * @return FatalErrorHandlerInterface[] An array of FatalErrorHandlerInterface + */ + protected function getFatalErrorHandlers() + { + return [ + new UndefinedFunctionFatalErrorHandler(), + new UndefinedMethodFatalErrorHandler(), + new ClassNotFoundFatalErrorHandler(), + ]; + } + + /** + * Cleans the trace by removing function arguments and the frames added by the error handler and DebugClassLoader. + */ + private function cleanTrace($backtrace, $type, $file, $line, $throw) + { + $lightTrace = $backtrace; + + 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; + } + } + if (class_exists(DebugClassLoader::class, false)) { + for ($i = \count($lightTrace) - 2; 0 < $i; --$i) { + if (DebugClassLoader::class === ($lightTrace[$i]['class'] ?? null)) { + array_splice($lightTrace, --$i, 2); + } + } + } + if (!($throw || $this->scopedErrors & $type)) { + for ($i = 0; isset($lightTrace[$i]); ++$i) { + unset($lightTrace[$i]['args'], $lightTrace[$i]['object']); + } + } + + return $lightTrace; + } } diff --git a/src/Symfony/Component/Debug/Exception/ClassNotFoundException.php b/src/Symfony/Component/Debug/Exception/ClassNotFoundException.php index 4b42cc61e4..fa98c4975d 100644 --- a/src/Symfony/Component/Debug/Exception/ClassNotFoundException.php +++ b/src/Symfony/Component/Debug/Exception/ClassNotFoundException.php @@ -11,13 +11,26 @@ namespace Symfony\Component\Debug\Exception; -use Symfony\Component\ErrorCatcher\Exception\ClassNotFoundException as BaseClassNotFoundException; - -@trigger_error(sprintf('The "%s" class is deprecated since Symfony 4.4, use "%s" instead.', ClassNotFoundException::class, BaseClassNotFoundException::class), E_USER_DEPRECATED); - /** - * @deprecated since Symfony 4.4, use Symfony\Component\ErrorCatcher\Exception\ClassNotFoundException instead. + * Class (or Trait or Interface) Not Found Exception. + * + * @author Konstanton Myakshin */ -class ClassNotFoundException extends BaseClassNotFoundException +class ClassNotFoundException extends FatalErrorException { + public function __construct(string $message, \ErrorException $previous) + { + parent::__construct( + $message, + $previous->getCode(), + $previous->getSeverity(), + $previous->getFile(), + $previous->getLine(), + null, + true, + null, + $previous->getPrevious() + ); + $this->setTrace($previous->getTrace()); + } } diff --git a/src/Symfony/Component/Debug/Exception/FatalErrorException.php b/src/Symfony/Component/Debug/Exception/FatalErrorException.php index 09a156ad7a..93880fbc32 100644 --- a/src/Symfony/Component/Debug/Exception/FatalErrorException.php +++ b/src/Symfony/Component/Debug/Exception/FatalErrorException.php @@ -11,13 +11,67 @@ namespace Symfony\Component\Debug\Exception; -use Symfony\Component\ErrorCatcher\Exception\FatalErrorException as BaseFatalErrorException; - -@trigger_error(sprintf('The "%s" class is deprecated since Symfony 4.4, use "%s" instead.', FatalErrorException::class, BaseFatalErrorException::class), E_USER_DEPRECATED); - /** - * @deprecated since Symfony 4.4, use Symfony\Component\ErrorCatcher\Exception\FatalErrorException instead. + * Fatal Error Exception. + * + * @author Konstanton Myakshin */ -class FatalErrorException extends BaseFatalErrorException +class FatalErrorException extends \ErrorException { + public function __construct(string $message, int $code, int $severity, string $filename, int $lineno, int $traceOffset = null, bool $traceArgs = true, array $trace = null, \Throwable $previous = null) + { + parent::__construct($message, $code, $severity, $filename, $lineno, $previous); + + if (null !== $trace) { + if (!$traceArgs) { + foreach ($trace as &$frame) { + unset($frame['args'], $frame['this'], $frame); + } + } + + $this->setTrace($trace); + } elseif (null !== $traceOffset) { + if (\function_exists('xdebug_get_function_stack')) { + $trace = xdebug_get_function_stack(); + if (0 < $traceOffset) { + array_splice($trace, -$traceOffset); + } + + foreach ($trace as &$frame) { + if (!isset($frame['type'])) { + // XDebug pre 2.1.1 doesn't currently set the call type key http://bugs.xdebug.org/view.php?id=695 + if (isset($frame['class'])) { + $frame['type'] = '::'; + } + } elseif ('dynamic' === $frame['type']) { + $frame['type'] = '->'; + } elseif ('static' === $frame['type']) { + $frame['type'] = '::'; + } + + // XDebug also has a different name for the parameters array + if (!$traceArgs) { + unset($frame['params'], $frame['args']); + } elseif (isset($frame['params']) && !isset($frame['args'])) { + $frame['args'] = $frame['params']; + unset($frame['params']); + } + } + + unset($frame); + $trace = array_reverse($trace); + } else { + $trace = []; + } + + $this->setTrace($trace); + } + } + + protected function setTrace($trace) + { + $traceReflector = new \ReflectionProperty('Exception', 'trace'); + $traceReflector->setAccessible(true); + $traceReflector->setValue($this, $trace); + } } diff --git a/src/Symfony/Component/Debug/Exception/FatalThrowableError.php b/src/Symfony/Component/Debug/Exception/FatalThrowableError.php index 6046f1c7cb..cdafb2a568 100644 --- a/src/Symfony/Component/Debug/Exception/FatalThrowableError.php +++ b/src/Symfony/Component/Debug/Exception/FatalThrowableError.php @@ -11,13 +11,41 @@ namespace Symfony\Component\Debug\Exception; -use Symfony\Component\ErrorCatcher\Exception\FatalThrowableError as BaseFatalThrowableError; - -@trigger_error(sprintf('The "%s" class is deprecated since Symfony 4.4, use "%s" instead.', FatalThrowableError::class, BaseFatalThrowableError::class), E_USER_DEPRECATED); - /** - * @deprecated since Symfony 4.4, use Symfony\Component\ErrorCatcher\Exception\FatalThrowableError instead. + * Fatal Throwable Error. + * + * @author Nicolas Grekas */ -class FatalThrowableError extends BaseFatalThrowableError +class FatalThrowableError extends FatalErrorException { + private $originalClassName; + + public function __construct(\Throwable $e) + { + $this->originalClassName = \get_class($e); + + if ($e instanceof \ParseError) { + $severity = E_PARSE; + } elseif ($e instanceof \TypeError) { + $severity = E_RECOVERABLE_ERROR; + } else { + $severity = E_ERROR; + } + + \ErrorException::__construct( + $e->getMessage(), + $e->getCode(), + $severity, + $e->getFile(), + $e->getLine(), + $e->getPrevious() + ); + + $this->setTrace($e->getTrace()); + } + + public function getOriginalClassName(): string + { + return $this->originalClassName; + } } diff --git a/src/Symfony/Component/Debug/Exception/FlattenException.php b/src/Symfony/Component/Debug/Exception/FlattenException.php index 748e2c6011..8dda8f7223 100644 --- a/src/Symfony/Component/Debug/Exception/FlattenException.php +++ b/src/Symfony/Component/Debug/Exception/FlattenException.php @@ -11,22 +11,358 @@ namespace Symfony\Component\Debug\Exception; -use Symfony\Component\ErrorCatcher\Exception\FlattenException as BaseFlattenException; +use Symfony\Component\HttpFoundation\Exception\RequestExceptionInterface; +use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface; -@trigger_error(sprintf('The "%s" class is deprecated since Symfony 4.4, use "%s" instead.', FlattenException::class, BaseFlattenException::class), E_USER_DEPRECATED); +@trigger_error(sprintf('The "%s" class is deprecated since Symfony 4.4, use "Symfony\Component\ErrorCatcher\Exception\FlattenException" instead.', FlattenException::class), E_USER_DEPRECATED); /** + * FlattenException wraps a PHP Error or Exception to be able to serialize it. + * + * Basically, this class removes all objects from the trace. + * + * @author Fabien Potencier + * * @deprecated since Symfony 4.4, use Symfony\Component\ErrorCatcher\Exception\FlattenException instead. */ -class FlattenException extends BaseFlattenException +class FlattenException { - /** - * @deprecated since Symfony 4.4, use Symfony\Component\ErrorHandler\Exception::createFromThrowable() instead. - */ - public static function create(\Exception $exception, $statusCode = null, array $headers = []): self - { - @trigger_error(sprintf('The "%s()" method is deprecated since Symfony 4.4, use Symfony\Component\ErrorHandler\Exception::createFromThrowable() instead.', __METHOD__), E_USER_DEPRECATED); + private $message; + private $code; + private $previous; + private $trace; + private $traceAsString; + private $class; + private $statusCode; + private $headers; + private $file; + private $line; - return parent::createFromThrowable($exception, $statusCode, $headers); + /** + * @deprecated since Symfony 4.4, use Symfony\Component\ErrorCatcher\Exception::createFromThrowable() instead. + */ + public static function create(\Exception $exception, $statusCode = null, array $headers = []) + { + @trigger_error(sprintf('The "%s()" method is deprecated since Symfony 4.4, use Symfony\Component\ErrorCatcher\Exception::createFromThrowable() instead.', __METHOD__), E_USER_DEPRECATED); + + return static::createFromThrowable($exception, $statusCode, $headers); + } + + public static function createFromThrowable(\Throwable $exception, int $statusCode = null, array $headers = []): self + { + $e = new static(); + $e->setMessage($exception->getMessage()); + $e->setCode($exception->getCode()); + + if ($exception instanceof HttpExceptionInterface) { + $statusCode = $exception->getStatusCode(); + $headers = array_merge($headers, $exception->getHeaders()); + } elseif ($exception instanceof RequestExceptionInterface) { + $statusCode = 400; + } + + if (null === $statusCode) { + $statusCode = 500; + } + + $e->setStatusCode($statusCode); + $e->setHeaders($headers); + $e->setTraceFromThrowable($exception); + $e->setClass($exception instanceof FatalThrowableError ? $exception->getOriginalClassName() : \get_class($exception)); + $e->setFile($exception->getFile()); + $e->setLine($exception->getLine()); + + $previous = $exception->getPrevious(); + + if ($previous instanceof \Throwable) { + $e->setPrevious(static::createFromThrowable($previous)); + } + + return $e; + } + + public function toArray() + { + $exceptions = []; + foreach (array_merge([$this], $this->getAllPrevious()) as $exception) { + $exceptions[] = [ + 'message' => $exception->getMessage(), + 'class' => $exception->getClass(), + 'trace' => $exception->getTrace(), + ]; + } + + return $exceptions; + } + + public function getStatusCode() + { + return $this->statusCode; + } + + /** + * @return $this + */ + public function setStatusCode($code) + { + $this->statusCode = $code; + + return $this; + } + + public function getHeaders() + { + return $this->headers; + } + + /** + * @return $this + */ + public function setHeaders(array $headers) + { + $this->headers = $headers; + + return $this; + } + + public function getClass() + { + return $this->class; + } + + /** + * @return $this + */ + public function setClass($class) + { + $this->class = 'c' === $class[0] && 0 === strpos($class, "class@anonymous\0") ? get_parent_class($class).'@anonymous' : $class; + + return $this; + } + + public function getFile() + { + return $this->file; + } + + /** + * @return $this + */ + public function setFile($file) + { + $this->file = $file; + + return $this; + } + + public function getLine() + { + return $this->line; + } + + /** + * @return $this + */ + public function setLine($line) + { + $this->line = $line; + + return $this; + } + + public function getMessage() + { + return $this->message; + } + + /** + * @return $this + */ + public function setMessage($message) + { + if (false !== strpos($message, "class@anonymous\0")) { + $message = preg_replace_callback('/class@anonymous\x00.*?\.php0x?[0-9a-fA-F]++/', function ($m) { + return class_exists($m[0], false) ? get_parent_class($m[0]).'@anonymous' : $m[0]; + }, $message); + } + + $this->message = $message; + + return $this; + } + + public function getCode() + { + return $this->code; + } + + /** + * @return $this + */ + public function setCode($code) + { + $this->code = $code; + + return $this; + } + + public function getPrevious() + { + return $this->previous; + } + + /** + * @return $this + */ + public function setPrevious(self $previous) + { + $this->previous = $previous; + + return $this; + } + + public function getAllPrevious() + { + $exceptions = []; + $e = $this; + while ($e = $e->getPrevious()) { + $exceptions[] = $e; + } + + return $exceptions; + } + + public function getTrace() + { + return $this->trace; + } + + /** + * @deprecated since 4.1, use {@see setTraceFromThrowable()} instead. + */ + public function setTraceFromException(\Exception $exception) + { + @trigger_error(sprintf('The "%s()" method is deprecated since Symfony 4.1, use "setTraceFromThrowable()" instead.', __METHOD__), E_USER_DEPRECATED); + + $this->setTraceFromThrowable($exception); + } + + public function setTraceFromThrowable(\Throwable $throwable) + { + $this->traceAsString = $throwable->getTraceAsString(); + + return $this->setTrace($throwable->getTrace(), $throwable->getFile(), $throwable->getLine()); + } + + /** + * @return $this + */ + public function setTrace($trace, $file, $line) + { + $this->trace = []; + $this->trace[] = [ + 'namespace' => '', + 'short_class' => '', + 'class' => '', + 'type' => '', + 'function' => '', + 'file' => $file, + 'line' => $line, + 'args' => [], + ]; + foreach ($trace as $entry) { + $class = ''; + $namespace = ''; + if (isset($entry['class'])) { + $parts = explode('\\', $entry['class']); + $class = array_pop($parts); + $namespace = implode('\\', $parts); + } + + $this->trace[] = [ + 'namespace' => $namespace, + 'short_class' => $class, + 'class' => isset($entry['class']) ? $entry['class'] : '', + 'type' => isset($entry['type']) ? $entry['type'] : '', + 'function' => isset($entry['function']) ? $entry['function'] : null, + 'file' => isset($entry['file']) ? $entry['file'] : null, + 'line' => isset($entry['line']) ? $entry['line'] : null, + 'args' => isset($entry['args']) ? $this->flattenArgs($entry['args']) : [], + ]; + } + + return $this; + } + + private function flattenArgs($args, $level = 0, &$count = 0) + { + $result = []; + foreach ($args as $key => $value) { + if (++$count > 1e4) { + return ['array', '*SKIPPED over 10000 entries*']; + } + if ($value instanceof \__PHP_Incomplete_Class) { + // is_object() returns false on PHP<=7.1 + $result[$key] = ['incomplete-object', $this->getClassNameFromIncomplete($value)]; + } elseif (\is_object($value)) { + $result[$key] = ['object', \get_class($value)]; + } elseif (\is_array($value)) { + if ($level > 10) { + $result[$key] = ['array', '*DEEP NESTED ARRAY*']; + } else { + $result[$key] = ['array', $this->flattenArgs($value, $level + 1, $count)]; + } + } elseif (null === $value) { + $result[$key] = ['null', null]; + } elseif (\is_bool($value)) { + $result[$key] = ['boolean', $value]; + } elseif (\is_int($value)) { + $result[$key] = ['integer', $value]; + } elseif (\is_float($value)) { + $result[$key] = ['float', $value]; + } elseif (\is_resource($value)) { + $result[$key] = ['resource', get_resource_type($value)]; + } else { + $result[$key] = ['string', (string) $value]; + } + } + + return $result; + } + + private function getClassNameFromIncomplete(\__PHP_Incomplete_Class $value) + { + $array = new \ArrayObject($value); + + return $array['__PHP_Incomplete_Class_Name']; + } + + public function getTraceAsString() + { + return $this->traceAsString; + } + + public function getAsString() + { + $message = ''; + $next = false; + + foreach (array_reverse(array_merge([$this], $this->getAllPrevious())) as $exception) { + if ($next) { + $message .= 'Next '; + } else { + $next = true; + } + $message .= $exception->getClass(); + + if ('' != $exception->getMessage()) { + $message .= ': '.$exception->getMessage(); + } + + $message .= ' in '.$exception->getFile().':'.$exception->getLine(). + "\nStack trace:\n".$exception->getTraceAsString()."\n\n"; + } + + return rtrim($message); } } diff --git a/src/Symfony/Component/Debug/Exception/OutOfMemoryException.php b/src/Symfony/Component/Debug/Exception/OutOfMemoryException.php index 20eb185242..fec1979836 100644 --- a/src/Symfony/Component/Debug/Exception/OutOfMemoryException.php +++ b/src/Symfony/Component/Debug/Exception/OutOfMemoryException.php @@ -11,13 +11,11 @@ namespace Symfony\Component\Debug\Exception; -use Symfony\Component\ErrorCatcher\Exception\OutOfMemoryException as BaseOutOfMemoryException; - -@trigger_error(sprintf('The "%s" class is deprecated since Symfony 4.4, use "%s" instead.', OutOfMemoryException::class, BaseOutOfMemoryException::class), E_USER_DEPRECATED); - /** - * @deprecated since Symfony 4.4, use Symfony\Component\ErrorCatcher\Exception\OutOfMemoryException instead. + * Out of memory exception. + * + * @author Nicolas Grekas */ -class OutOfMemoryException extends BaseOutOfMemoryException +class OutOfMemoryException extends FatalErrorException { } diff --git a/src/Symfony/Component/Debug/Exception/SilencedErrorContext.php b/src/Symfony/Component/Debug/Exception/SilencedErrorContext.php index 2b8ddc2bcb..236c56ed0e 100644 --- a/src/Symfony/Component/Debug/Exception/SilencedErrorContext.php +++ b/src/Symfony/Component/Debug/Exception/SilencedErrorContext.php @@ -11,13 +11,57 @@ namespace Symfony\Component\Debug\Exception; -use Symfony\Component\ErrorCatcher\Exception\SilencedErrorContext as BaseSilencedErrorContext; - -@trigger_error(sprintf('The "%s" class is deprecated since Symfony 4.4, use "%s" instead.', SilencedErrorContext::class, BaseSilencedErrorContext::class), E_USER_DEPRECATED); - /** - * @deprecated since Symfony 4.4, use Symfony\Component\ErrorCatcher\Exception\SilencedErrorContext instead. + * Data Object that represents a Silenced Error. + * + * @author Grégoire Pineau */ -class SilencedErrorContext extends BaseSilencedErrorContext +class SilencedErrorContext implements \JsonSerializable { + public $count = 1; + + private $severity; + private $file; + private $line; + private $trace; + + public function __construct(int $severity, string $file, int $line, array $trace = [], int $count = 1) + { + $this->severity = $severity; + $this->file = $file; + $this->line = $line; + $this->trace = $trace; + $this->count = $count; + } + + public function getSeverity() + { + return $this->severity; + } + + public function getFile() + { + return $this->file; + } + + public function getLine() + { + return $this->line; + } + + public function getTrace() + { + return $this->trace; + } + + public function JsonSerialize() + { + return [ + 'severity' => $this->severity, + 'file' => $this->file, + 'line' => $this->line, + 'trace' => $this->trace, + 'count' => $this->count, + ]; + } } diff --git a/src/Symfony/Component/Debug/Exception/UndefinedFunctionException.php b/src/Symfony/Component/Debug/Exception/UndefinedFunctionException.php index 9d50933862..d936c8759e 100644 --- a/src/Symfony/Component/Debug/Exception/UndefinedFunctionException.php +++ b/src/Symfony/Component/Debug/Exception/UndefinedFunctionException.php @@ -11,13 +11,26 @@ namespace Symfony\Component\Debug\Exception; -use Symfony\Component\ErrorCatcher\Exception\UndefinedFunctionException as BaseUndefinedFunctionException; - -@trigger_error(sprintf('The "%s" class is deprecated since Symfony 4.4, use "%s" instead.', UndefinedFunctionException::class, BaseUndefinedFunctionException::class), E_USER_DEPRECATED); - /** - * @deprecated since Symfony 4.4, use Symfony\Component\ErrorCatcher\Exception\UndefinedFunctionException instead. + * Undefined Function Exception. + * + * @author Konstanton Myakshin */ -class UndefinedFunctionException extends BaseUndefinedFunctionException +class UndefinedFunctionException extends FatalErrorException { + public function __construct(string $message, \ErrorException $previous) + { + parent::__construct( + $message, + $previous->getCode(), + $previous->getSeverity(), + $previous->getFile(), + $previous->getLine(), + null, + true, + null, + $previous->getPrevious() + ); + $this->setTrace($previous->getTrace()); + } } diff --git a/src/Symfony/Component/Debug/Exception/UndefinedMethodException.php b/src/Symfony/Component/Debug/Exception/UndefinedMethodException.php index 0edba83249..f627561fe1 100644 --- a/src/Symfony/Component/Debug/Exception/UndefinedMethodException.php +++ b/src/Symfony/Component/Debug/Exception/UndefinedMethodException.php @@ -11,13 +11,26 @@ namespace Symfony\Component\Debug\Exception; -use Symfony\Component\ErrorCatcher\Exception\UndefinedMethodException as BaseUndefinedMethodException; - -@trigger_error(sprintf('The "%s" class is deprecated since Symfony 4.4, use "%s" instead.', UndefinedMethodException::class, BaseUndefinedMethodException::class), E_USER_DEPRECATED); - /** - * @deprecated since Symfony 4.4, use Symfony\Component\ErrorCatcher\Exception\UndefinedMethodException instead. + * Undefined Method Exception. + * + * @author Grégoire Pineau */ -class UndefinedMethodException extends BaseUndefinedMethodException +class UndefinedMethodException extends FatalErrorException { + public function __construct(string $message, \ErrorException $previous) + { + parent::__construct( + $message, + $previous->getCode(), + $previous->getSeverity(), + $previous->getFile(), + $previous->getLine(), + null, + true, + null, + $previous->getPrevious() + ); + $this->setTrace($previous->getTrace()); + } } diff --git a/src/Symfony/Component/Debug/ExceptionHandler.php b/src/Symfony/Component/Debug/ExceptionHandler.php index d195319ddf..e36610c82e 100644 --- a/src/Symfony/Component/Debug/ExceptionHandler.php +++ b/src/Symfony/Component/Debug/ExceptionHandler.php @@ -11,13 +11,456 @@ namespace Symfony\Component\Debug; -use Symfony\Component\ErrorCatcher\ExceptionHandler as BaseExceptionHandler; - -@trigger_error(sprintf('The "%s" class is deprecated since Symfony 4.4, use "%s" instead.', ExceptionHandler::class, BaseExceptionHandler::class), E_USER_DEPRECATED); +use Symfony\Component\Debug\Exception\FlattenException; +use Symfony\Component\Debug\Exception\OutOfMemoryException; +use Symfony\Component\HttpKernel\Debug\FileLinkFormatter; /** - * @deprecated since Symfony 4.4, use Symfony\Component\ErrorCatcher\ExceptionHandler instead. + * ExceptionHandler converts an exception to a Response object. + * + * It is mostly useful in debug mode to replace the default PHP/XDebug + * output with something prettier and more useful. + * + * As this class is mainly used during Kernel boot, where nothing is yet + * available, the Response content is always HTML. + * + * @author Fabien Potencier + * @author Nicolas Grekas + * + * @final since Symfony 4.3 */ -class ExceptionHandler extends BaseExceptionHandler +class ExceptionHandler { + private const GHOST_ADDONS = [ + '02-14' => self::GHOST_HEART, + '02-29' => self::GHOST_PLUS, + '10-18' => self::GHOST_GIFT, + ]; + + private const GHOST_GIFT = 'M124.005 5.36c.396-.715 1.119-1.648-.124-1.873-.346-.177-.692-.492-1.038-.141-.769.303-1.435.728-.627 1.523.36.514.685 1.634 1.092 1.758.242-.417.47-.842.697-1.266zm-1.699 1.977c-.706-1.26-1.274-2.612-2.138-3.774-1.051-1.123-3.122-.622-3.593.825-.625 1.431.724 3.14 2.251 2.96 1.159.02 2.324.072 3.48-.011zm5.867.043c1.502-.202 2.365-2.092 1.51-3.347-.757-1.34-2.937-1.387-3.698-.025-.659 1.1-1.23 2.25-1.835 3.38 1.336.077 2.686.06 4.023-.008zm2.487 1.611c.512-.45 2.494-.981.993-1.409-.372-.105-.805-.59-1.14-.457-.726.902-1.842 1.432-3.007 1.376-.228.075-1.391-.114-1.077.1.822.47 1.623.979 2.474 1.395.595-.317 1.173-.667 1.757-1.005zm-11.696.255l1.314-.765c-1.338-.066-2.87.127-3.881-.95-.285-.319-.559-.684-.954-.282-.473.326-1.929.66-.808 1.058.976.576 1.945 1.167 2.946 1.701.476-.223.926-.503 1.383-.762zm6.416 2.846c.567-.456 1.942-.89 1.987-1.38-1.282-.737-2.527-1.56-3.87-2.183-.461-.175-.835.094-1.207.328-1.1.654-2.225 1.267-3.288 1.978 1.39.86 2.798 1.695 4.219 2.504.725-.407 1.44-.83 2.16-1.247zm5.692 1.423l1.765-1.114c-.005-1.244.015-2.488-.019-3.732a77.306 77.306 0 0 0-3.51 2.084c-.126 1.282-.062 2.586-.034 3.876.607-.358 1.2-.741 1.798-1.114zm-13.804-.784c.06-1.06.19-2.269-1.09-2.583-.807-.376-1.926-1.341-2.548-1.332-.02 1.195-.01 2.39-.011 3.585 1.192.744 2.364 1.524 3.582 2.226.119-.616.041-1.269.067-1.896zm8.541 4.105l2.117-1.336c-.003-1.284.05-2.57-.008-3.853-.776.223-1.662.91-2.48 1.337l-1.834 1.075c.012 1.37-.033 2.744.044 4.113.732-.427 1.443-.887 2.161-1.336zm-2.957-.72v-2.057c-1.416-.828-2.828-1.664-4.25-2.482-.078 1.311-.033 2.627-.045 3.94 1.416.887 2.817 1.798 4.25 2.655.057-.683.036-1.372.045-2.057zm8.255 2.755l1.731-1.153c-.024-1.218.06-2.453-.062-3.658-1.2.685-2.358 1.464-3.537 2.195.028 1.261-.058 2.536.072 3.786.609-.373 1.2-.777 1.796-1.17zm-13.851-.683l-.014-1.916c-1.193-.746-2.37-1.517-3.58-2.234-.076 1.224-.033 2.453-.044 3.679 1.203.796 2.392 1.614 3.61 2.385.048-.636.024-1.276.028-1.914zm8.584 4.199l2.102-1.396c-.002-1.298.024-2.596-.01-3.893-1.427.88-2.843 1.775-4.25 2.686-.158 1.253-.055 2.545-.056 3.811.437.266 1.553-.912 2.214-1.208zm-2.988-.556c-.085-.894.365-2.154-.773-2.5-1.146-.727-2.288-1.46-3.45-2.163-.17 1.228.008 2.508-.122 3.751a79.399 79.399 0 0 0 4.278 2.885c.117-.641.044-1.32.067-1.973zm-4.872-.236l-5.087-3.396c.002-3.493-.047-6.988.015-10.48.85-.524 1.753-.954 2.627-1.434-.564-1.616.25-3.58 1.887-4.184 1.372-.563 3.025-.055 3.9 1.13l1.906-.978 1.916.987c.915-1.086 2.483-1.706 3.842-1.097 1.631.573 2.52 2.532 1.936 4.145.88.497 1.837.886 2.644 1.492.036 3.473 0 6.946-.003 10.419-3.374 2.233-6.693 4.55-10.122 6.699-.997 0-1.858-1.083-2.783-1.522a735.316 735.316 0 0 1-2.678-1.781z'; + private const GHOST_HEART = 'M125.914 8.305c3.036-8.71 14.933 0 0 11.2-14.932-11.2-3.036-19.91 0-11.2z'; + private const GHOST_PLUS = 'M111.368 8.97h7.324V1.645h7.512v7.323h7.324v7.513h-7.324v7.323h-7.512v-7.323h-7.324z'; + + private $debug; + private $charset; + private $handler; + private $caughtBuffer; + private $caughtLength; + private $fileLinkFormat; + + public function __construct(bool $debug = true, string $charset = null, $fileLinkFormat = null) + { + $this->debug = $debug; + $this->charset = $charset ?: ini_get('default_charset') ?: 'UTF-8'; + $this->fileLinkFormat = $fileLinkFormat; + } + + /** + * Registers the exception handler. + * + * @param bool $debug Enable/disable debug mode, where the stack trace is displayed + * @param string|null $charset The charset used by exception messages + * @param string|null $fileLinkFormat The IDE link template + * + * @return static + */ + public static function register($debug = true, $charset = null, $fileLinkFormat = null) + { + $handler = new static($debug, $charset, $fileLinkFormat); + + $prev = set_exception_handler([$handler, 'handle']); + if (\is_array($prev) && $prev[0] instanceof ErrorHandler) { + restore_exception_handler(); + $prev[0]->setExceptionHandler([$handler, 'handle']); + } + + return $handler; + } + + /** + * Sets a user exception handler. + * + * @param callable $handler An handler that will be called on Exception + * + * @return callable|null The previous exception handler if any + */ + public function setHandler(callable $handler = null) + { + $old = $this->handler; + $this->handler = $handler; + + return $old; + } + + /** + * Sets the format for links to source files. + * + * @param string|FileLinkFormatter $fileLinkFormat The format for links to source files + * + * @return string The previous file link format + */ + public function setFileLinkFormat($fileLinkFormat) + { + $old = $this->fileLinkFormat; + $this->fileLinkFormat = $fileLinkFormat; + + return $old; + } + + /** + * Sends a response for the given Exception. + * + * To be as fail-safe as possible, the exception is first handled + * by our simple exception handler, then by the user exception handler. + * The latter takes precedence and any output from the former is cancelled, + * if and only if nothing bad happens in this handling path. + */ + public function handle(\Exception $exception) + { + if (null === $this->handler || $exception instanceof OutOfMemoryException) { + $this->sendPhpResponse($exception); + + return; + } + + $caughtLength = $this->caughtLength = 0; + + ob_start(function ($buffer) { + $this->caughtBuffer = $buffer; + + return ''; + }); + + $this->sendPhpResponse($exception); + while (null === $this->caughtBuffer && ob_end_flush()) { + // Empty loop, everything is in the condition + } + if (isset($this->caughtBuffer[0])) { + ob_start(function ($buffer) { + if ($this->caughtLength) { + // use substr_replace() instead of substr() for mbstring overloading resistance + $cleanBuffer = substr_replace($buffer, '', 0, $this->caughtLength); + if (isset($cleanBuffer[0])) { + $buffer = $cleanBuffer; + } + } + + return $buffer; + }); + + echo $this->caughtBuffer; + $caughtLength = ob_get_length(); + } + $this->caughtBuffer = null; + + try { + ($this->handler)($exception); + $this->caughtLength = $caughtLength; + } catch (\Exception $e) { + if (!$caughtLength) { + // All handlers failed. Let PHP handle that now. + throw $exception; + } + } + } + + /** + * Sends the error associated with the given Exception as a plain PHP response. + * + * This method uses plain PHP functions like header() and echo to output + * the response. + * + * @param \Throwable|FlattenException $exception A \Throwable or FlattenException instance + */ + public function sendPhpResponse($exception) + { + if ($exception instanceof \Throwable) { + $exception = FlattenException::createFromThrowable($exception); + } + + if (!headers_sent()) { + header(sprintf('HTTP/1.0 %s', $exception->getStatusCode())); + foreach ($exception->getHeaders() as $name => $value) { + header($name.': '.$value, false); + } + header('Content-Type: text/html; charset='.$this->charset); + } + + echo $this->decorate($this->getContent($exception), $this->getStylesheet($exception)); + } + + /** + * Gets the full HTML content associated with the given exception. + * + * @param \Exception|FlattenException $exception An \Exception or FlattenException instance + * + * @return string The HTML content as a string + */ + public function getHtml($exception) + { + if (!$exception instanceof FlattenException) { + $exception = FlattenException::create($exception); + } + + return $this->decorate($this->getContent($exception), $this->getStylesheet($exception)); + } + + /** + * Gets the HTML content associated with the given exception. + * + * @return string The content as a string + */ + public function getContent(FlattenException $exception) + { + switch ($exception->getStatusCode()) { + case 404: + $title = 'Sorry, the page you are looking for could not be found.'; + break; + default: + $title = $this->debug ? $this->escapeHtml($exception->getMessage()) : 'Whoops, looks like something went wrong.'; + } + + if (!$this->debug) { + return << +

$title

+ +EOF; + } + + $content = ''; + try { + $count = \count($exception->getAllPrevious()); + $total = $count + 1; + foreach ($exception->toArray() as $position => $e) { + $ind = $count - $position + 1; + $class = $this->formatClass($e['class']); + $message = nl2br($this->escapeHtml($e['message'])); + $content .= sprintf(<<<'EOF' +
+ + + +EOF + , $ind, $total, $class, $message); + foreach ($e['trace'] as $trace) { + $content .= '\n"; + } + + $content .= "\n
+

+ (%d/%d) + %s +

+

%s

+
'; + if ($trace['function']) { + $content .= sprintf('at %s%s%s(%s)', $this->formatClass($trace['class']), $trace['type'], $trace['function'], $this->formatArgs($trace['args'])); + } + if (isset($trace['file']) && isset($trace['line'])) { + $content .= $this->formatPath($trace['file'], $trace['line']); + } + $content .= "
\n
\n"; + } + } catch (\Exception $e) { + // something nasty happened and we cannot throw an exception anymore + if ($this->debug) { + $e = FlattenException::create($e); + $title = sprintf('Exception thrown when handling an exception (%s: %s)', $e->getClass(), $this->escapeHtml($e->getMessage())); + } else { + $title = 'Whoops, looks like something went wrong.'; + } + } + + $symfonyGhostImageContents = $this->getSymfonyGhostAsSvg(); + + return << +
+
+

$title

+
$symfonyGhostImageContents
+
+
+ + +
+ $content +
+EOF; + } + + /** + * Gets the stylesheet associated with the given exception. + * + * @return string The stylesheet as a string + */ + public function getStylesheet(FlattenException $exception) + { + if (!$this->debug) { + return <<<'EOF' + body { background-color: #fff; color: #222; font: 16px/1.5 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; margin: 0; } + .container { margin: 30px; max-width: 600px; } + h1 { color: #dc3545; font-size: 24px; } +EOF; + } + + return <<<'EOF' + body { background-color: #F9F9F9; color: #222; font: 14px/1.4 Helvetica, Arial, sans-serif; margin: 0; padding-bottom: 45px; } + + a { cursor: pointer; text-decoration: none; } + a:hover { text-decoration: underline; } + abbr[title] { border-bottom: none; cursor: help; text-decoration: none; } + + code, pre { font: 13px/1.5 Consolas, Monaco, Menlo, "Ubuntu Mono", "Liberation Mono", monospace; } + + table, tr, th, td { background: #FFF; border-collapse: collapse; vertical-align: top; } + table { background: #FFF; border: 1px solid #E0E0E0; box-shadow: 0px 0px 1px rgba(128, 128, 128, .2); margin: 1em 0; width: 100%; } + table th, table td { border: solid #E0E0E0; border-width: 1px 0; padding: 8px 10px; } + table th { background-color: #E0E0E0; font-weight: bold; text-align: left; } + + .hidden-xs-down { display: none; } + .block { display: block; } + .break-long-words { -ms-word-break: break-all; word-break: break-all; word-break: break-word; -webkit-hyphens: auto; -moz-hyphens: auto; hyphens: auto; } + .text-muted { color: #999; } + + .container { max-width: 1024px; margin: 0 auto; padding: 0 15px; } + .container::after { content: ""; display: table; clear: both; } + + .exception-summary { background: #B0413E; border-bottom: 2px solid rgba(0, 0, 0, 0.1); border-top: 1px solid rgba(0, 0, 0, .3); flex: 0 0 auto; margin-bottom: 30px; } + + .exception-message-wrapper { display: flex; align-items: center; min-height: 70px; } + .exception-message { flex-grow: 1; padding: 30px 0; } + .exception-message, .exception-message a { color: #FFF; font-size: 21px; font-weight: 400; margin: 0; } + .exception-message.long { font-size: 18px; } + .exception-message a { border-bottom: 1px solid rgba(255, 255, 255, 0.5); font-size: inherit; text-decoration: none; } + .exception-message a:hover { border-bottom-color: #ffffff; } + + .exception-illustration { flex-basis: 111px; flex-shrink: 0; height: 66px; margin-left: 15px; opacity: .7; } + + .trace + .trace { margin-top: 30px; } + .trace-head .trace-class { color: #222; font-size: 18px; font-weight: bold; line-height: 1.3; margin: 0; position: relative; } + + .trace-message { font-size: 14px; font-weight: normal; margin: .5em 0 0; } + + .trace-file-path, .trace-file-path a { color: #222; margin-top: 3px; font-size: 13px; } + .trace-class { color: #B0413E; } + .trace-type { padding: 0 2px; } + .trace-method { color: #B0413E; font-weight: bold; } + .trace-arguments { color: #777; font-weight: normal; padding-left: 2px; } + + @media (min-width: 575px) { + .hidden-xs-down { display: initial; } + } +EOF; + } + + private function decorate($content, $css) + { + return << + + + + + + + + $content + + +EOF; + } + + private function formatClass($class) + { + $parts = explode('\\', $class); + + return sprintf('%s', $class, array_pop($parts)); + } + + private function formatPath($path, $line) + { + $file = $this->escapeHtml(preg_match('#[^/\\\\]*+$#', $path, $file) ? $file[0] : $path); + $fmt = $this->fileLinkFormat ?: ini_get('xdebug.file_link_format') ?: get_cfg_var('xdebug.file_link_format'); + + if (!$fmt) { + return sprintf('in %s%s', $this->escapeHtml($path), $file, 0 < $line ? ' line '.$line : ''); + } + + if (\is_string($fmt)) { + $i = strpos($f = $fmt, '&', max(strrpos($f, '%f'), strrpos($f, '%l'))) ?: \strlen($f); + $fmt = [substr($f, 0, $i)] + preg_split('/&([^>]++)>/', substr($f, $i), -1, PREG_SPLIT_DELIM_CAPTURE); + + for ($i = 1; isset($fmt[$i]); ++$i) { + if (0 === strpos($path, $k = $fmt[$i++])) { + $path = substr_replace($path, $fmt[$i], 0, \strlen($k)); + break; + } + } + + $link = strtr($fmt[0], ['%f' => $path, '%l' => $line]); + } else { + try { + $link = $fmt->format($path, $line); + } catch (\Exception $e) { + return sprintf('in %s%s', $this->escapeHtml($path), $file, 0 < $line ? ' line '.$line : ''); + } + } + + return sprintf('in %s%s', $this->escapeHtml($link), $file, 0 < $line ? ' line '.$line : ''); + } + + /** + * Formats an array as a string. + * + * @param array $args The argument array + * + * @return string + */ + private function formatArgs(array $args) + { + $result = []; + foreach ($args as $key => $item) { + if ('object' === $item[0]) { + $formattedValue = sprintf('object(%s)', $this->formatClass($item[1])); + } elseif ('array' === $item[0]) { + $formattedValue = sprintf('array(%s)', \is_array($item[1]) ? $this->formatArgs($item[1]) : $item[1]); + } elseif ('null' === $item[0]) { + $formattedValue = 'null'; + } elseif ('boolean' === $item[0]) { + $formattedValue = ''.strtolower(var_export($item[1], true)).''; + } elseif ('resource' === $item[0]) { + $formattedValue = 'resource'; + } else { + $formattedValue = str_replace("\n", '', $this->escapeHtml(var_export($item[1], true))); + } + + $result[] = \is_int($key) ? $formattedValue : sprintf("'%s' => %s", $this->escapeHtml($key), $formattedValue); + } + + return implode(', ', $result); + } + + /** + * HTML-encodes a string. + */ + private function escapeHtml($str) + { + return htmlspecialchars($str, ENT_COMPAT | ENT_SUBSTITUTE, $this->charset); + } + + private function getSymfonyGhostAsSvg() + { + return ''.$this->addElementToGhost().''; + } + + private function addElementToGhost() + { + if (!isset(self::GHOST_ADDONS[date('m-d')])) { + return ''; + } + + return ''; + } } diff --git a/src/Symfony/Component/Debug/FatalErrorHandler/ClassNotFoundFatalErrorHandler.php b/src/Symfony/Component/Debug/FatalErrorHandler/ClassNotFoundFatalErrorHandler.php index e7f1285f98..a0e2f770f0 100644 --- a/src/Symfony/Component/Debug/FatalErrorHandler/ClassNotFoundFatalErrorHandler.php +++ b/src/Symfony/Component/Debug/FatalErrorHandler/ClassNotFoundFatalErrorHandler.php @@ -11,13 +11,183 @@ namespace Symfony\Component\Debug\FatalErrorHandler; -use Symfony\Component\ErrorCatcher\FatalErrorHandler\ClassNotFoundFatalErrorHandler as BaseClassNotFoundFatalErrorHandler; - -@trigger_error(sprintf('The "%s" class is deprecated since Symfony 4.4, use "%s" instead.', ClassNotFoundFatalErrorHandler::class, BaseClassNotFoundFatalErrorHandler::class), E_USER_DEPRECATED); +use Composer\Autoload\ClassLoader as ComposerClassLoader; +use Symfony\Component\ClassLoader\ClassLoader as SymfonyClassLoader; +use Symfony\Component\Debug\DebugClassLoader; +use Symfony\Component\Debug\Exception\ClassNotFoundException; +use Symfony\Component\Debug\Exception\FatalErrorException; /** - * @deprecated since Symfony 4.4, use Symfony\Component\ErrorCatcher\FatalErrorHandler\ClassNotFoundFatalErrorHandler instead. + * ErrorHandler for classes that do not exist. + * + * @author Fabien Potencier */ -class ClassNotFoundFatalErrorHandler extends BaseClassNotFoundFatalErrorHandler +class ClassNotFoundFatalErrorHandler implements FatalErrorHandlerInterface { + /** + * {@inheritdoc} + */ + public function handleError(array $error, FatalErrorException $exception) + { + $messageLen = \strlen($error['message']); + $notFoundSuffix = '\' not found'; + $notFoundSuffixLen = \strlen($notFoundSuffix); + if ($notFoundSuffixLen > $messageLen) { + return; + } + + if (0 !== substr_compare($error['message'], $notFoundSuffix, -$notFoundSuffixLen)) { + return; + } + + foreach (['class', 'interface', 'trait'] as $typeName) { + $prefix = ucfirst($typeName).' \''; + $prefixLen = \strlen($prefix); + if (0 !== strpos($error['message'], $prefix)) { + continue; + } + + $fullyQualifiedClassName = substr($error['message'], $prefixLen, -$notFoundSuffixLen); + if (false !== $namespaceSeparatorIndex = strrpos($fullyQualifiedClassName, '\\')) { + $className = substr($fullyQualifiedClassName, $namespaceSeparatorIndex + 1); + $namespacePrefix = substr($fullyQualifiedClassName, 0, $namespaceSeparatorIndex); + $message = sprintf('Attempted to load %s "%s" from namespace "%s".', $typeName, $className, $namespacePrefix); + $tail = ' for another namespace?'; + } else { + $className = $fullyQualifiedClassName; + $message = sprintf('Attempted to load %s "%s" from the global namespace.', $typeName, $className); + $tail = '?'; + } + + if ($candidates = $this->getClassCandidates($className)) { + $tail = array_pop($candidates).'"?'; + if ($candidates) { + $tail = ' for e.g. "'.implode('", "', $candidates).'" or "'.$tail; + } else { + $tail = ' for "'.$tail; + } + } + $message .= "\nDid you forget a \"use\" statement".$tail; + + return new ClassNotFoundException($message, $exception); + } + } + + /** + * Tries to guess the full namespace for a given class name. + * + * By default, it looks for PSR-0 and PSR-4 classes registered via a Symfony or a Composer + * autoloader (that should cover all common cases). + * + * @param string $class A class name (without its namespace) + * + * @return array An array of possible fully qualified class names + */ + private function getClassCandidates(string $class): array + { + if (!\is_array($functions = spl_autoload_functions())) { + return []; + } + + // find Symfony and Composer autoloaders + $classes = []; + + foreach ($functions as $function) { + if (!\is_array($function)) { + continue; + } + // get class loaders wrapped by DebugClassLoader + if ($function[0] instanceof DebugClassLoader) { + $function = $function[0]->getClassLoader(); + + if (!\is_array($function)) { + continue; + } + } + + if ($function[0] instanceof ComposerClassLoader || $function[0] instanceof SymfonyClassLoader) { + foreach ($function[0]->getPrefixes() as $prefix => $paths) { + foreach ($paths as $path) { + $classes = array_merge($classes, $this->findClassInPath($path, $class, $prefix)); + } + } + } + if ($function[0] instanceof ComposerClassLoader) { + foreach ($function[0]->getPrefixesPsr4() as $prefix => $paths) { + foreach ($paths as $path) { + $classes = array_merge($classes, $this->findClassInPath($path, $class, $prefix)); + } + } + } + } + + return array_unique($classes); + } + + private function findClassInPath(string $path, string $class, string $prefix): array + { + if (!$path = realpath($path.'/'.strtr($prefix, '\\_', '//')) ?: realpath($path.'/'.\dirname(strtr($prefix, '\\_', '//'))) ?: realpath($path)) { + return []; + } + + $classes = []; + $filename = $class.'.php'; + foreach (new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($path, \RecursiveDirectoryIterator::SKIP_DOTS), \RecursiveIteratorIterator::LEAVES_ONLY) as $file) { + if ($filename == $file->getFileName() && $class = $this->convertFileToClass($path, $file->getPathName(), $prefix)) { + $classes[] = $class; + } + } + + return $classes; + } + + private function convertFileToClass(string $path, string $file, string $prefix): ?string + { + $candidates = [ + // namespaced class + $namespacedClass = str_replace([$path.\DIRECTORY_SEPARATOR, '.php', '/'], ['', '', '\\'], $file), + // namespaced class (with target dir) + $prefix.$namespacedClass, + // namespaced class (with target dir and separator) + $prefix.'\\'.$namespacedClass, + // PEAR class + str_replace('\\', '_', $namespacedClass), + // PEAR class (with target dir) + str_replace('\\', '_', $prefix.$namespacedClass), + // PEAR class (with target dir and separator) + str_replace('\\', '_', $prefix.'\\'.$namespacedClass), + ]; + + if ($prefix) { + $candidates = array_filter($candidates, function ($candidate) use ($prefix) { return 0 === strpos($candidate, $prefix); }); + } + + // We cannot use the autoloader here as most of them use require; but if the class + // is not found, the new autoloader call will require the file again leading to a + // "cannot redeclare class" error. + foreach ($candidates as $candidate) { + if ($this->classExists($candidate)) { + return $candidate; + } + } + + try { + require_once $file; + } catch (\Throwable $e) { + return null; + } + + foreach ($candidates as $candidate) { + if ($this->classExists($candidate)) { + return $candidate; + } + } + + return null; + } + + private function classExists(string $class): bool + { + return class_exists($class, false) || interface_exists($class, false) || trait_exists($class, false); + } } diff --git a/src/Symfony/Component/Debug/FatalErrorHandler/FatalErrorHandlerInterface.php b/src/Symfony/Component/Debug/FatalErrorHandler/FatalErrorHandlerInterface.php index 701ebe7640..6b87eb30a1 100644 --- a/src/Symfony/Component/Debug/FatalErrorHandler/FatalErrorHandlerInterface.php +++ b/src/Symfony/Component/Debug/FatalErrorHandler/FatalErrorHandlerInterface.php @@ -11,13 +11,22 @@ namespace Symfony\Component\Debug\FatalErrorHandler; -use Symfony\Component\ErrorCatcher\FatalErrorHandler\FatalErrorHandlerInterface as BaseFatalErrorHandlerInterface; - -@trigger_error(sprintf('The "%s" interface is deprecated since Symfony 4.4, use "%s" instead.', FatalErrorHandlerInterface::class, BaseFatalErrorHandlerInterface::class), E_USER_DEPRECATED); +use Symfony\Component\Debug\Exception\FatalErrorException; /** - * @deprecated since Symfony 4.4, use Symfony\Component\ErrorCatcher\FatalErrorHandler\FatalErrorHandlerInterface instead. + * Attempts to convert fatal errors to exceptions. + * + * @author Fabien Potencier */ -interface FatalErrorHandlerInterface extends BaseFatalErrorHandlerInterface +interface FatalErrorHandlerInterface { + /** + * Attempts to convert an error into an exception. + * + * @param array $error An array as returned by error_get_last() + * @param FatalErrorException $exception A FatalErrorException instance + * + * @return FatalErrorException|null A FatalErrorException instance if the class is able to convert the error, null otherwise + */ + public function handleError(array $error, FatalErrorException $exception); } diff --git a/src/Symfony/Component/Debug/FatalErrorHandler/UndefinedFunctionFatalErrorHandler.php b/src/Symfony/Component/Debug/FatalErrorHandler/UndefinedFunctionFatalErrorHandler.php index 51015368e0..9eddeba5a6 100644 --- a/src/Symfony/Component/Debug/FatalErrorHandler/UndefinedFunctionFatalErrorHandler.php +++ b/src/Symfony/Component/Debug/FatalErrorHandler/UndefinedFunctionFatalErrorHandler.php @@ -11,13 +11,74 @@ namespace Symfony\Component\Debug\FatalErrorHandler; -use Symfony\Component\ErrorCatcher\FatalErrorHandler\UndefinedFunctionFatalErrorHandler as BaseUndefinedFunctionFatalErrorHandler; - -@trigger_error(sprintf('The "%s" class is deprecated since Symfony 4.4, use "%s" instead.', UndefinedFunctionFatalErrorHandler::class, BaseUndefinedFunctionFatalErrorHandler::class), E_USER_DEPRECATED); +use Symfony\Component\Debug\Exception\FatalErrorException; +use Symfony\Component\Debug\Exception\UndefinedFunctionException; /** - * @deprecated since Symfony 4.4, use Symfony\Component\ErrorCatcher\FatalErrorHandler\UndefinedFunctionFatalErrorHandler instead. + * ErrorHandler for undefined functions. + * + * @author Fabien Potencier */ -class UndefinedFunctionFatalErrorHandler extends BaseUndefinedFunctionFatalErrorHandler +class UndefinedFunctionFatalErrorHandler implements FatalErrorHandlerInterface { + /** + * {@inheritdoc} + */ + public function handleError(array $error, FatalErrorException $exception) + { + $messageLen = \strlen($error['message']); + $notFoundSuffix = '()'; + $notFoundSuffixLen = \strlen($notFoundSuffix); + if ($notFoundSuffixLen > $messageLen) { + return; + } + + if (0 !== substr_compare($error['message'], $notFoundSuffix, -$notFoundSuffixLen)) { + return; + } + + $prefix = 'Call to undefined function '; + $prefixLen = \strlen($prefix); + if (0 !== strpos($error['message'], $prefix)) { + return; + } + + $fullyQualifiedFunctionName = substr($error['message'], $prefixLen, -$notFoundSuffixLen); + if (false !== $namespaceSeparatorIndex = strrpos($fullyQualifiedFunctionName, '\\')) { + $functionName = substr($fullyQualifiedFunctionName, $namespaceSeparatorIndex + 1); + $namespacePrefix = substr($fullyQualifiedFunctionName, 0, $namespaceSeparatorIndex); + $message = sprintf('Attempted to call function "%s" from namespace "%s".', $functionName, $namespacePrefix); + } else { + $functionName = $fullyQualifiedFunctionName; + $message = sprintf('Attempted to call function "%s" from the global namespace.', $functionName); + } + + $candidates = []; + foreach (get_defined_functions() as $type => $definedFunctionNames) { + foreach ($definedFunctionNames as $definedFunctionName) { + if (false !== $namespaceSeparatorIndex = strrpos($definedFunctionName, '\\')) { + $definedFunctionNameBasename = substr($definedFunctionName, $namespaceSeparatorIndex + 1); + } else { + $definedFunctionNameBasename = $definedFunctionName; + } + + if ($definedFunctionNameBasename === $functionName) { + $candidates[] = '\\'.$definedFunctionName; + } + } + } + + if ($candidates) { + sort($candidates); + $last = array_pop($candidates).'"?'; + if ($candidates) { + $candidates = 'e.g. "'.implode('", "', $candidates).'" or "'.$last; + } else { + $candidates = '"'.$last; + } + $message .= "\nDid you mean to call ".$candidates; + } + + return new UndefinedFunctionException($message, $exception); + } } diff --git a/src/Symfony/Component/Debug/FatalErrorHandler/UndefinedMethodFatalErrorHandler.php b/src/Symfony/Component/Debug/FatalErrorHandler/UndefinedMethodFatalErrorHandler.php index 9e9164e07f..1318cb13ba 100644 --- a/src/Symfony/Component/Debug/FatalErrorHandler/UndefinedMethodFatalErrorHandler.php +++ b/src/Symfony/Component/Debug/FatalErrorHandler/UndefinedMethodFatalErrorHandler.php @@ -11,13 +11,56 @@ namespace Symfony\Component\Debug\FatalErrorHandler; -use Symfony\Component\ErrorCatcher\FatalErrorHandler\UndefinedMethodFatalErrorHandler as BaseUndefinedMethodFatalErrorHandler; - -@trigger_error(sprintf('The "%s" class is deprecated since Symfony 4.4, use "%s" instead.', UndefinedMethodFatalErrorHandler::class, BaseUndefinedMethodFatalErrorHandler::class), E_USER_DEPRECATED); +use Symfony\Component\Debug\Exception\FatalErrorException; +use Symfony\Component\Debug\Exception\UndefinedMethodException; /** - * @deprecated since Symfony 4.4, use Symfony\Component\ErrorCatcher\FatalErrorHandler\UndefinedMethodFatalErrorHandler instead. + * ErrorHandler for undefined methods. + * + * @author Grégoire Pineau */ -class UndefinedMethodFatalErrorHandler extends BaseUndefinedMethodFatalErrorHandler +class UndefinedMethodFatalErrorHandler implements FatalErrorHandlerInterface { + /** + * {@inheritdoc} + */ + public function handleError(array $error, FatalErrorException $exception) + { + preg_match('/^Call to undefined method (.*)::(.*)\(\)$/', $error['message'], $matches); + if (!$matches) { + return; + } + + $className = $matches[1]; + $methodName = $matches[2]; + + $message = sprintf('Attempted to call an undefined method named "%s" of class "%s".', $methodName, $className); + + if (!class_exists($className) || null === $methods = get_class_methods($className)) { + // failed to get the class or its methods on which an unknown method was called (for example on an anonymous class) + return new UndefinedMethodException($message, $exception); + } + + $candidates = []; + foreach ($methods as $definedMethodName) { + $lev = levenshtein($methodName, $definedMethodName); + if ($lev <= \strlen($methodName) / 3 || false !== strpos($definedMethodName, $methodName)) { + $candidates[] = $definedMethodName; + } + } + + if ($candidates) { + sort($candidates); + $last = array_pop($candidates).'"?'; + if ($candidates) { + $candidates = 'e.g. "'.implode('", "', $candidates).'" or "'.$last; + } else { + $candidates = '"'.$last; + } + + $message .= "\nDid you mean to call ".$candidates; + } + + return new UndefinedMethodException($message, $exception); + } } diff --git a/src/Symfony/Component/ErrorCatcher/Tests/ErrorHandlerTest.php b/src/Symfony/Component/Debug/Tests/ErrorHandlerTest.php similarity index 97% rename from src/Symfony/Component/ErrorCatcher/Tests/ErrorHandlerTest.php rename to src/Symfony/Component/Debug/Tests/ErrorHandlerTest.php index d1d845a866..f758d21e0e 100644 --- a/src/Symfony/Component/ErrorCatcher/Tests/ErrorHandlerTest.php +++ b/src/Symfony/Component/Debug/Tests/ErrorHandlerTest.php @@ -9,16 +9,16 @@ * file that was distributed with this source code. */ -namespace Symfony\Component\ErrorCatcher\Tests; +namespace Symfony\Component\Debug\Tests; use PHPUnit\Framework\TestCase; use Psr\Log\LogLevel; use Psr\Log\NullLogger; -use Symfony\Component\ErrorCatcher\BufferingLogger; -use Symfony\Component\ErrorCatcher\ErrorHandler; -use Symfony\Component\ErrorCatcher\Exception\SilencedErrorContext; -use Symfony\Component\ErrorCatcher\Tests\Fixtures\ErrorHandlerThatUsesThePreviousOne; -use Symfony\Component\ErrorCatcher\Tests\Fixtures\LoggerThatSetAnErrorHandler; +use Symfony\Component\Debug\BufferingLogger; +use Symfony\Component\Debug\ErrorHandler; +use Symfony\Component\Debug\Exception\SilencedErrorContext; +use Symfony\Component\Debug\Tests\Fixtures\ErrorHandlerThatUsesThePreviousOne; +use Symfony\Component\Debug\Tests\Fixtures\LoggerThatSetAnErrorHandler; /** * ErrorHandlerTest. @@ -33,7 +33,7 @@ class ErrorHandlerTest extends TestCase $handler = ErrorHandler::register(); try { - $this->assertInstanceOf('Symfony\Component\ErrorCatcher\ErrorHandler', $handler); + $this->assertInstanceOf('Symfony\Component\Debug\ErrorHandler', $handler); $this->assertSame($handler, ErrorHandler::register()); $newHandler = new ErrorHandler(); @@ -151,21 +151,21 @@ class ErrorHandlerTest extends TestCase $handler->setDefaultLogger($logger, [E_USER_NOTICE => LogLevel::CRITICAL]); $loggers = [ - E_COMPILE_ERROR => [null, LogLevel::CRITICAL], - E_COMPILE_WARNING => [null, LogLevel::WARNING], - E_CORE_ERROR => [null, LogLevel::CRITICAL], - E_CORE_WARNING => [null, LogLevel::WARNING], E_DEPRECATED => [null, LogLevel::INFO], - E_ERROR => [null, LogLevel::CRITICAL], - E_NOTICE => [$logger, LogLevel::WARNING], - E_PARSE => [null, LogLevel::CRITICAL], - E_RECOVERABLE_ERROR => [null, LogLevel::CRITICAL], - E_STRICT => [null, LogLevel::WARNING], E_USER_DEPRECATED => [null, LogLevel::INFO], - E_USER_ERROR => [null, LogLevel::CRITICAL], + E_NOTICE => [$logger, LogLevel::WARNING], E_USER_NOTICE => [$logger, LogLevel::CRITICAL], - E_USER_WARNING => [null, LogLevel::WARNING], + E_STRICT => [null, LogLevel::WARNING], E_WARNING => [null, LogLevel::WARNING], + E_USER_WARNING => [null, LogLevel::WARNING], + E_COMPILE_WARNING => [null, LogLevel::WARNING], + E_CORE_WARNING => [null, LogLevel::WARNING], + E_USER_ERROR => [null, LogLevel::CRITICAL], + E_RECOVERABLE_ERROR => [null, LogLevel::CRITICAL], + E_COMPILE_ERROR => [null, LogLevel::CRITICAL], + E_PARSE => [null, LogLevel::CRITICAL], + E_ERROR => [null, LogLevel::CRITICAL], + E_CORE_ERROR => [null, LogLevel::CRITICAL], ]; $this->assertSame($loggers, $handler->setLoggers([])); } finally { @@ -375,21 +375,21 @@ class ErrorHandlerTest extends TestCase $handler = new ErrorHandler($bootLogger); $loggers = [ - E_COMPILE_ERROR => [$bootLogger, LogLevel::CRITICAL], - E_COMPILE_WARNING => [$bootLogger, LogLevel::WARNING], - E_CORE_ERROR => [$bootLogger, LogLevel::CRITICAL], - E_CORE_WARNING => [$bootLogger, LogLevel::WARNING], E_DEPRECATED => [$bootLogger, LogLevel::INFO], - E_ERROR => [$bootLogger, LogLevel::CRITICAL], - E_NOTICE => [$bootLogger, LogLevel::WARNING], - E_PARSE => [$bootLogger, LogLevel::CRITICAL], - E_RECOVERABLE_ERROR => [$bootLogger, LogLevel::CRITICAL], - E_STRICT => [$bootLogger, LogLevel::WARNING], E_USER_DEPRECATED => [$bootLogger, LogLevel::INFO], - E_USER_ERROR => [$bootLogger, LogLevel::CRITICAL], + E_NOTICE => [$bootLogger, LogLevel::WARNING], E_USER_NOTICE => [$bootLogger, LogLevel::WARNING], - E_USER_WARNING => [$bootLogger, LogLevel::WARNING], + E_STRICT => [$bootLogger, LogLevel::WARNING], E_WARNING => [$bootLogger, LogLevel::WARNING], + E_USER_WARNING => [$bootLogger, LogLevel::WARNING], + E_COMPILE_WARNING => [$bootLogger, LogLevel::WARNING], + E_CORE_WARNING => [$bootLogger, LogLevel::WARNING], + E_USER_ERROR => [$bootLogger, LogLevel::CRITICAL], + E_RECOVERABLE_ERROR => [$bootLogger, LogLevel::CRITICAL], + E_COMPILE_ERROR => [$bootLogger, LogLevel::CRITICAL], + E_PARSE => [$bootLogger, LogLevel::CRITICAL], + E_ERROR => [$bootLogger, LogLevel::CRITICAL], + E_CORE_ERROR => [$bootLogger, LogLevel::CRITICAL], ]; $this->assertSame($loggers, $handler->setLoggers([])); @@ -490,7 +490,7 @@ class ErrorHandlerTest extends TestCase $handler->handleException($exception); - $this->assertInstanceOf('Symfony\Component\ErrorCatcher\Exception\ClassNotFoundException', $args[0]); + $this->assertInstanceOf('Symfony\Component\Debug\Exception\ClassNotFoundException', $args[0]); $this->assertStringStartsWith("Attempted to load class \"IReallyReallyDoNotExistAnywhereInTheRepositoryISwear\" from the global namespace.\nDid you forget a \"use\" statement", $args[0]->getMessage()); } diff --git a/src/Symfony/Component/Debug/Tests/Exception/FlattenExceptionTest.php b/src/Symfony/Component/Debug/Tests/Exception/FlattenExceptionTest.php new file mode 100644 index 0000000000..f18809a5a2 --- /dev/null +++ b/src/Symfony/Component/Debug/Tests/Exception/FlattenExceptionTest.php @@ -0,0 +1,391 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Debug\Tests\Exception; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\Debug\Exception\FatalThrowableError; +use Symfony\Component\Debug\Exception\FlattenException; +use Symfony\Component\HttpFoundation\Exception\SuspiciousOperationException; +use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; +use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; +use Symfony\Component\HttpKernel\Exception\ConflictHttpException; +use Symfony\Component\HttpKernel\Exception\GoneHttpException; +use Symfony\Component\HttpKernel\Exception\LengthRequiredHttpException; +use Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException; +use Symfony\Component\HttpKernel\Exception\NotAcceptableHttpException; +use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; +use Symfony\Component\HttpKernel\Exception\PreconditionFailedHttpException; +use Symfony\Component\HttpKernel\Exception\PreconditionRequiredHttpException; +use Symfony\Component\HttpKernel\Exception\ServiceUnavailableHttpException; +use Symfony\Component\HttpKernel\Exception\TooManyRequestsHttpException; +use Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException; +use Symfony\Component\HttpKernel\Exception\UnsupportedMediaTypeHttpException; + +/** + * @group legacy + */ +class FlattenExceptionTest extends TestCase +{ + public function testStatusCode() + { + $flattened = FlattenException::create(new \RuntimeException(), 403); + $this->assertEquals('403', $flattened->getStatusCode()); + + $flattened = FlattenException::create(new \RuntimeException()); + $this->assertEquals('500', $flattened->getStatusCode()); + + $flattened = FlattenException::createFromThrowable(new \DivisionByZeroError(), 403); + $this->assertEquals('403', $flattened->getStatusCode()); + + $flattened = FlattenException::createFromThrowable(new \DivisionByZeroError()); + $this->assertEquals('500', $flattened->getStatusCode()); + + $flattened = FlattenException::create(new NotFoundHttpException()); + $this->assertEquals('404', $flattened->getStatusCode()); + + $flattened = FlattenException::create(new UnauthorizedHttpException('Basic realm="My Realm"')); + $this->assertEquals('401', $flattened->getStatusCode()); + + $flattened = FlattenException::create(new BadRequestHttpException()); + $this->assertEquals('400', $flattened->getStatusCode()); + + $flattened = FlattenException::create(new NotAcceptableHttpException()); + $this->assertEquals('406', $flattened->getStatusCode()); + + $flattened = FlattenException::create(new ConflictHttpException()); + $this->assertEquals('409', $flattened->getStatusCode()); + + $flattened = FlattenException::create(new MethodNotAllowedHttpException(['POST'])); + $this->assertEquals('405', $flattened->getStatusCode()); + + $flattened = FlattenException::create(new AccessDeniedHttpException()); + $this->assertEquals('403', $flattened->getStatusCode()); + + $flattened = FlattenException::create(new GoneHttpException()); + $this->assertEquals('410', $flattened->getStatusCode()); + + $flattened = FlattenException::create(new LengthRequiredHttpException()); + $this->assertEquals('411', $flattened->getStatusCode()); + + $flattened = FlattenException::create(new PreconditionFailedHttpException()); + $this->assertEquals('412', $flattened->getStatusCode()); + + $flattened = FlattenException::create(new PreconditionRequiredHttpException()); + $this->assertEquals('428', $flattened->getStatusCode()); + + $flattened = FlattenException::create(new ServiceUnavailableHttpException()); + $this->assertEquals('503', $flattened->getStatusCode()); + + $flattened = FlattenException::create(new TooManyRequestsHttpException()); + $this->assertEquals('429', $flattened->getStatusCode()); + + $flattened = FlattenException::create(new UnsupportedMediaTypeHttpException()); + $this->assertEquals('415', $flattened->getStatusCode()); + + if (class_exists(SuspiciousOperationException::class)) { + $flattened = FlattenException::create(new SuspiciousOperationException()); + $this->assertEquals('400', $flattened->getStatusCode()); + } + } + + public function testHeadersForHttpException() + { + $flattened = FlattenException::create(new MethodNotAllowedHttpException(['POST'])); + $this->assertEquals(['Allow' => 'POST'], $flattened->getHeaders()); + + $flattened = FlattenException::create(new UnauthorizedHttpException('Basic realm="My Realm"')); + $this->assertEquals(['WWW-Authenticate' => 'Basic realm="My Realm"'], $flattened->getHeaders()); + + $flattened = FlattenException::create(new ServiceUnavailableHttpException('Fri, 31 Dec 1999 23:59:59 GMT')); + $this->assertEquals(['Retry-After' => 'Fri, 31 Dec 1999 23:59:59 GMT'], $flattened->getHeaders()); + + $flattened = FlattenException::create(new ServiceUnavailableHttpException(120)); + $this->assertEquals(['Retry-After' => 120], $flattened->getHeaders()); + + $flattened = FlattenException::create(new TooManyRequestsHttpException('Fri, 31 Dec 1999 23:59:59 GMT')); + $this->assertEquals(['Retry-After' => 'Fri, 31 Dec 1999 23:59:59 GMT'], $flattened->getHeaders()); + + $flattened = FlattenException::create(new TooManyRequestsHttpException(120)); + $this->assertEquals(['Retry-After' => 120], $flattened->getHeaders()); + } + + /** + * @dataProvider flattenDataProvider + */ + public function testFlattenHttpException(\Throwable $exception) + { + $flattened = FlattenException::createFromThrowable($exception); + $flattened2 = FlattenException::createFromThrowable($exception); + + $flattened->setPrevious($flattened2); + + $this->assertEquals($exception->getMessage(), $flattened->getMessage(), 'The message is copied from the original exception.'); + $this->assertEquals($exception->getCode(), $flattened->getCode(), 'The code is copied from the original exception.'); + $this->assertInstanceOf($flattened->getClass(), $exception, 'The class is set to the class of the original exception'); + } + + public function testWrappedThrowable() + { + $exception = new FatalThrowableError(new \DivisionByZeroError('Ouch', 42)); + $flattened = FlattenException::create($exception); + + $this->assertSame('Ouch', $flattened->getMessage(), 'The message is copied from the original error.'); + $this->assertSame(42, $flattened->getCode(), 'The code is copied from the original error.'); + $this->assertSame('DivisionByZeroError', $flattened->getClass(), 'The class is set to the class of the original error'); + } + + public function testThrowable() + { + $error = new \DivisionByZeroError('Ouch', 42); + $flattened = FlattenException::createFromThrowable($error); + + $this->assertSame('Ouch', $flattened->getMessage(), 'The message is copied from the original error.'); + $this->assertSame(42, $flattened->getCode(), 'The code is copied from the original error.'); + $this->assertSame('DivisionByZeroError', $flattened->getClass(), 'The class is set to the class of the original error'); + } + + /** + * @dataProvider flattenDataProvider + */ + public function testPrevious(\Throwable $exception) + { + $flattened = FlattenException::createFromThrowable($exception); + $flattened2 = FlattenException::createFromThrowable($exception); + + $flattened->setPrevious($flattened2); + + $this->assertSame($flattened2, $flattened->getPrevious()); + + $this->assertSame([$flattened2], $flattened->getAllPrevious()); + } + + public function testPreviousError() + { + $exception = new \Exception('test', 123, new \ParseError('Oh noes!', 42)); + + $flattened = FlattenException::create($exception)->getPrevious(); + + $this->assertEquals($flattened->getMessage(), 'Oh noes!', 'The message is copied from the original exception.'); + $this->assertEquals($flattened->getCode(), 42, 'The code is copied from the original exception.'); + $this->assertEquals($flattened->getClass(), 'ParseError', 'The class is set to the class of the original exception'); + } + + /** + * @dataProvider flattenDataProvider + */ + public function testLine(\Throwable $exception) + { + $flattened = FlattenException::createFromThrowable($exception); + $this->assertSame($exception->getLine(), $flattened->getLine()); + } + + /** + * @dataProvider flattenDataProvider + */ + public function testFile(\Throwable $exception) + { + $flattened = FlattenException::createFromThrowable($exception); + $this->assertSame($exception->getFile(), $flattened->getFile()); + } + + /** + * @dataProvider flattenDataProvider + */ + public function testToArray(\Throwable $exception, string $expectedClass) + { + $flattened = FlattenException::createFromThrowable($exception); + $flattened->setTrace([], 'foo.php', 123); + + $this->assertEquals([ + [ + 'message' => 'test', + 'class' => $expectedClass, + 'trace' => [[ + 'namespace' => '', 'short_class' => '', 'class' => '', 'type' => '', 'function' => '', 'file' => 'foo.php', 'line' => 123, + 'args' => [], + ]], + ], + ], $flattened->toArray()); + } + + public function testCreate() + { + $exception = new NotFoundHttpException( + 'test', + new \RuntimeException('previous', 123) + ); + + $this->assertSame( + FlattenException::createFromThrowable($exception)->toArray(), + FlattenException::create($exception)->toArray() + ); + } + + public function flattenDataProvider() + { + return [ + [new \Exception('test', 123), 'Exception'], + [new \Error('test', 123), 'Error'], + ]; + } + + public function testArguments() + { + $dh = opendir(__DIR__); + $fh = tmpfile(); + + $incomplete = unserialize('O:14:"BogusTestClass":0:{}'); + + $exception = $this->createException([ + (object) ['foo' => 1], + new NotFoundHttpException(), + $incomplete, + $dh, + $fh, + function () {}, + [1, 2], + ['foo' => 123], + null, + true, + false, + 0, + 0.0, + '0', + '', + INF, + NAN, + ]); + + $flattened = FlattenException::create($exception); + $trace = $flattened->getTrace(); + $args = $trace[1]['args']; + $array = $args[0][1]; + + closedir($dh); + fclose($fh); + + $i = 0; + $this->assertSame(['object', 'stdClass'], $array[$i++]); + $this->assertSame(['object', 'Symfony\Component\HttpKernel\Exception\NotFoundHttpException'], $array[$i++]); + $this->assertSame(['incomplete-object', 'BogusTestClass'], $array[$i++]); + $this->assertSame(['resource', 'stream'], $array[$i++]); + $this->assertSame(['resource', 'stream'], $array[$i++]); + + $args = $array[$i++]; + $this->assertSame($args[0], 'object'); + $this->assertTrue('Closure' === $args[1] || is_subclass_of($args[1], '\Closure'), 'Expect object class name to be Closure or a subclass of Closure.'); + + $this->assertSame(['array', [['integer', 1], ['integer', 2]]], $array[$i++]); + $this->assertSame(['array', ['foo' => ['integer', 123]]], $array[$i++]); + $this->assertSame(['null', null], $array[$i++]); + $this->assertSame(['boolean', true], $array[$i++]); + $this->assertSame(['boolean', false], $array[$i++]); + $this->assertSame(['integer', 0], $array[$i++]); + $this->assertSame(['float', 0.0], $array[$i++]); + $this->assertSame(['string', '0'], $array[$i++]); + $this->assertSame(['string', ''], $array[$i++]); + $this->assertSame(['float', INF], $array[$i++]); + + // assertEquals() does not like NAN values. + $this->assertEquals($array[$i][0], 'float'); + $this->assertTrue(is_nan($array[$i++][1])); + } + + public function testRecursionInArguments() + { + $a = null; + $a = ['foo', [2, &$a]]; + $exception = $this->createException($a); + + $flattened = FlattenException::create($exception); + $trace = $flattened->getTrace(); + $this->assertContains('*DEEP NESTED ARRAY*', serialize($trace)); + } + + public function testTooBigArray() + { + $a = []; + for ($i = 0; $i < 20; ++$i) { + for ($j = 0; $j < 50; ++$j) { + for ($k = 0; $k < 10; ++$k) { + $a[$i][$j][$k] = 'value'; + } + } + } + $a[20] = 'value'; + $a[21] = 'value1'; + $exception = $this->createException($a); + + $flattened = FlattenException::create($exception); + $trace = $flattened->getTrace(); + + $this->assertSame($trace[1]['args'][0], ['array', ['array', '*SKIPPED over 10000 entries*']]); + + $serializeTrace = serialize($trace); + + $this->assertContains('*SKIPPED over 10000 entries*', $serializeTrace); + $this->assertNotContains('*value1*', $serializeTrace); + } + + public function testAnonymousClass() + { + $flattened = FlattenException::create(new class() extends \RuntimeException { + }); + + $this->assertSame('RuntimeException@anonymous', $flattened->getClass()); + + $flattened = FlattenException::create(new \Exception(sprintf('Class "%s" blah.', \get_class(new class() extends \RuntimeException { + })))); + + $this->assertSame('Class "RuntimeException@anonymous" blah.', $flattened->getMessage()); + } + + public function testToStringEmptyMessage() + { + $exception = new \RuntimeException(); + + $flattened = FlattenException::create($exception); + + $this->assertSame($exception->getTraceAsString(), $flattened->getTraceAsString()); + $this->assertSame($exception->__toString(), $flattened->getAsString()); + } + + public function testToString() + { + $test = function ($a, $b, $c, $d) { + return new \RuntimeException('This is a test message'); + }; + + $exception = $test('foo123', 1, null, 1.5); + + $flattened = FlattenException::create($exception); + + $this->assertSame($exception->getTraceAsString(), $flattened->getTraceAsString()); + $this->assertSame($exception->__toString(), $flattened->getAsString()); + } + + public function testToStringParent() + { + $exception = new \LogicException('This is message 1'); + $exception = new \RuntimeException('This is messsage 2', 500, $exception); + + $flattened = FlattenException::create($exception); + + $this->assertSame($exception->getTraceAsString(), $flattened->getTraceAsString()); + $this->assertSame($exception->__toString(), $flattened->getAsString()); + } + + private function createException($foo) + { + return new \Exception(); + } +} diff --git a/src/Symfony/Component/ErrorCatcher/Tests/ExceptionHandlerTest.php b/src/Symfony/Component/Debug/Tests/ExceptionHandlerTest.php similarity index 86% rename from src/Symfony/Component/ErrorCatcher/Tests/ExceptionHandlerTest.php rename to src/Symfony/Component/Debug/Tests/ExceptionHandlerTest.php index d3b8deec80..31f9a90bc4 100644 --- a/src/Symfony/Component/ErrorCatcher/Tests/ExceptionHandlerTest.php +++ b/src/Symfony/Component/Debug/Tests/ExceptionHandlerTest.php @@ -9,11 +9,11 @@ * file that was distributed with this source code. */ -namespace Symfony\Component\ErrorCatcher\Tests; +namespace Symfony\Component\Debug\Tests; use PHPUnit\Framework\TestCase; -use Symfony\Component\ErrorCatcher\Exception\OutOfMemoryException; -use Symfony\Component\ErrorCatcher\ExceptionHandler; +use Symfony\Component\Debug\Exception\OutOfMemoryException; +use Symfony\Component\Debug\ExceptionHandler; use Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; @@ -31,6 +31,9 @@ class ExceptionHandlerTest extends TestCase testHeader(); } + /** + * @group legacy + */ public function testDebug() { $handler = new ExceptionHandler(false); @@ -39,7 +42,7 @@ class ExceptionHandlerTest extends TestCase $handler->sendPhpResponse(new \RuntimeException('Foo')); $response = ob_get_clean(); - $this->assertContains('The server returned a "500 Internal Server Error".', $response); + $this->assertContains('Whoops, looks like something went wrong.', $response); $this->assertNotContains('
', $response); $handler = new ExceptionHandler(true); @@ -69,7 +72,7 @@ content="0;url=data:text/html;base64,PHNjcmlwdD5hbGVydCgndGVzdDMnKTwvc2NyaXB0Pg" $handler->sendPhpResponse(new NotFoundHttpException('Foo')); $response = ob_get_clean(); - $this->assertContains('The server returned a "404 Not Found".', $response); + $this->assertContains('Sorry, the page you are looking for could not be found.', $response); $expectedHeaders = [ ['HTTP/1.0 404', true, null], @@ -110,7 +113,7 @@ content="0;url=data:text/html;base64,PHNjcmlwdD5hbGVydCgndGVzdDMnKTwvc2NyaXB0Pg" { $exception = new \Exception('foo'); - $handler = $this->getMockBuilder('Symfony\Component\ErrorCatcher\ExceptionHandler')->setMethods(['sendPhpResponse'])->getMock(); + $handler = $this->getMockBuilder('Symfony\Component\Debug\ExceptionHandler')->setMethods(['sendPhpResponse'])->getMock(); $handler ->expects($this->exactly(2)) ->method('sendPhpResponse'); @@ -128,7 +131,7 @@ content="0;url=data:text/html;base64,PHNjcmlwdD5hbGVydCgndGVzdDMnKTwvc2NyaXB0Pg" { $exception = new OutOfMemoryException('foo', 0, E_ERROR, __FILE__, __LINE__); - $handler = $this->getMockBuilder('Symfony\Component\ErrorCatcher\ExceptionHandler')->setMethods(['sendPhpResponse'])->getMock(); + $handler = $this->getMockBuilder('Symfony\Component\Debug\ExceptionHandler')->setMethods(['sendPhpResponse'])->getMock(); $handler ->expects($this->once()) ->method('sendPhpResponse'); diff --git a/src/Symfony/Component/ErrorCatcher/Tests/FatalErrorHandler/ClassNotFoundFatalErrorHandlerTest.php b/src/Symfony/Component/Debug/Tests/FatalErrorHandler/ClassNotFoundFatalErrorHandlerTest.php similarity index 80% rename from src/Symfony/Component/ErrorCatcher/Tests/FatalErrorHandler/ClassNotFoundFatalErrorHandlerTest.php rename to src/Symfony/Component/Debug/Tests/FatalErrorHandler/ClassNotFoundFatalErrorHandlerTest.php index 0d02f6ca93..8e615ac640 100644 --- a/src/Symfony/Component/ErrorCatcher/Tests/FatalErrorHandler/ClassNotFoundFatalErrorHandlerTest.php +++ b/src/Symfony/Component/Debug/Tests/FatalErrorHandler/ClassNotFoundFatalErrorHandlerTest.php @@ -9,13 +9,13 @@ * file that was distributed with this source code. */ -namespace Symfony\Component\ErrorCatcher\Tests\FatalErrorHandler; +namespace Symfony\Component\Debug\Tests\FatalErrorHandler; use Composer\Autoload\ClassLoader as ComposerClassLoader; use PHPUnit\Framework\TestCase; use Symfony\Component\Debug\DebugClassLoader; -use Symfony\Component\ErrorCatcher\Exception\FatalErrorException; -use Symfony\Component\ErrorCatcher\FatalErrorHandler\ClassNotFoundFatalErrorHandler; +use Symfony\Component\Debug\Exception\FatalErrorException; +use Symfony\Component\Debug\FatalErrorHandler\ClassNotFoundFatalErrorHandler; class ClassNotFoundFatalErrorHandlerTest extends TestCase { @@ -32,7 +32,7 @@ class ClassNotFoundFatalErrorHandlerTest extends TestCase } if ($function[0] instanceof ComposerClassLoader) { - $function[0]->add('Symfony_Component_ErrorCatcher_Tests_Fixtures', \dirname(\dirname(\dirname(\dirname(\dirname(__DIR__)))))); + $function[0]->add('Symfony_Component_Debug_Tests_Fixtures', \dirname(\dirname(\dirname(\dirname(\dirname(__DIR__)))))); break; } } @@ -60,7 +60,7 @@ class ClassNotFoundFatalErrorHandlerTest extends TestCase array_map('spl_autoload_register', $autoloaders); } - $this->assertInstanceOf('Symfony\Component\ErrorCatcher\Exception\ClassNotFoundException', $exception); + $this->assertInstanceOf('Symfony\Component\Debug\Exception\ClassNotFoundException', $exception); $this->assertSame($translatedMessage, $exception->getMessage()); $this->assertSame($error['type'], $exception->getSeverity()); $this->assertSame($error['file'], $exception->getFile()); @@ -70,7 +70,7 @@ class ClassNotFoundFatalErrorHandlerTest extends TestCase public function provideClassNotFoundData() { $autoloader = new ComposerClassLoader(); - $autoloader->add('Symfony\Component\ErrorCatcher\Exception\\', realpath(__DIR__.'/../../Exception')); + $autoloader->add('Symfony\Component\Debug\Exception\\', realpath(__DIR__.'/../../Exception')); $debugClassLoader = new DebugClassLoader([$autoloader, 'loadClass']); @@ -98,9 +98,9 @@ class ClassNotFoundFatalErrorHandlerTest extends TestCase 'type' => 1, 'line' => 12, 'file' => 'foo.php', - 'message' => 'Class \'UndefinedFuncException\' not found', + 'message' => 'Class \'UndefinedFunctionException\' not found', ], - "Attempted to load class \"UndefinedFuncException\" from the global namespace.\nDid you forget a \"use\" statement for \"Symfony\Component\ErrorCatcher\Tests\Fixtures\UndefinedFuncException\"?", + "Attempted to load class \"UndefinedFunctionException\" from the global namespace.\nDid you forget a \"use\" statement for \"Symfony\Component\Debug\Exception\UndefinedFunctionException\"?", ], [ [ @@ -109,16 +109,7 @@ class ClassNotFoundFatalErrorHandlerTest extends TestCase 'file' => 'foo.php', 'message' => 'Class \'PEARClass\' not found', ], - "Attempted to load class \"PEARClass\" from the global namespace.\nDid you forget a \"use\" statement for \"Symfony_Component_ErrorCatcher_Tests_Fixtures_PEARClass\"?", - ], - [ - [ - 'type' => 1, - 'line' => 12, - 'file' => 'foo.php', - 'message' => 'Class \'Foo\\Bar\\UndefinedFuncException\' not found', - ], - "Attempted to load class \"UndefinedFuncException\" from namespace \"Foo\Bar\".\nDid you forget a \"use\" statement for \"Symfony\Component\ErrorCatcher\Tests\Fixtures\UndefinedFuncException\"?", + "Attempted to load class \"PEARClass\" from the global namespace.\nDid you forget a \"use\" statement for \"Symfony_Component_Debug_Tests_Fixtures_PEARClass\"?", ], [ [ @@ -127,7 +118,16 @@ class ClassNotFoundFatalErrorHandlerTest extends TestCase 'file' => 'foo.php', 'message' => 'Class \'Foo\\Bar\\UndefinedFunctionException\' not found', ], - "Attempted to load class \"UndefinedFunctionException\" from namespace \"Foo\Bar\".\nDid you forget a \"use\" statement for \"Symfony\Component\ErrorCatcher\Exception\UndefinedFunctionException\"?", + "Attempted to load class \"UndefinedFunctionException\" from namespace \"Foo\Bar\".\nDid you forget a \"use\" statement for \"Symfony\Component\Debug\Exception\UndefinedFunctionException\"?", + ], + [ + [ + 'type' => 1, + 'line' => 12, + 'file' => 'foo.php', + 'message' => 'Class \'Foo\\Bar\\UndefinedFunctionException\' not found', + ], + "Attempted to load class \"UndefinedFunctionException\" from namespace \"Foo\Bar\".\nDid you forget a \"use\" statement for \"Symfony\Component\Debug\Exception\UndefinedFunctionException\"?", [$autoloader, 'loadClass'], ], [ @@ -137,7 +137,7 @@ class ClassNotFoundFatalErrorHandlerTest extends TestCase 'file' => 'foo.php', 'message' => 'Class \'Foo\\Bar\\UndefinedFunctionException\' not found', ], - "Attempted to load class \"UndefinedFunctionException\" from namespace \"Foo\Bar\".\nDid you forget a \"use\" statement for \"Symfony\Component\ErrorCatcher\Exception\UndefinedFunctionException\"?", + "Attempted to load class \"UndefinedFunctionException\" from namespace \"Foo\Bar\".\nDid you forget a \"use\" statement for \"Symfony\Component\Debug\Exception\UndefinedFunctionException\"?", [$debugClassLoader, 'loadClass'], ], [ @@ -171,6 +171,6 @@ class ClassNotFoundFatalErrorHandlerTest extends TestCase $handler = new ClassNotFoundFatalErrorHandler(); $exception = $handler->handleError($error, new FatalErrorException('', 0, $error['type'], $error['file'], $error['line'])); - $this->assertInstanceOf('Symfony\Component\ErrorCatcher\Exception\ClassNotFoundException', $exception); + $this->assertInstanceOf('Symfony\Component\Debug\Exception\ClassNotFoundException', $exception); } } diff --git a/src/Symfony/Component/ErrorCatcher/Tests/FatalErrorHandler/UndefinedFunctionFatalErrorHandlerTest.php b/src/Symfony/Component/Debug/Tests/FatalErrorHandler/UndefinedFunctionFatalErrorHandlerTest.php similarity index 84% rename from src/Symfony/Component/ErrorCatcher/Tests/FatalErrorHandler/UndefinedFunctionFatalErrorHandlerTest.php rename to src/Symfony/Component/Debug/Tests/FatalErrorHandler/UndefinedFunctionFatalErrorHandlerTest.php index cf8a5fc319..de9994e447 100644 --- a/src/Symfony/Component/ErrorCatcher/Tests/FatalErrorHandler/UndefinedFunctionFatalErrorHandlerTest.php +++ b/src/Symfony/Component/Debug/Tests/FatalErrorHandler/UndefinedFunctionFatalErrorHandlerTest.php @@ -9,11 +9,11 @@ * file that was distributed with this source code. */ -namespace Symfony\Component\ErrorCatcher\Tests\FatalErrorHandler; +namespace Symfony\Component\Debug\Tests\FatalErrorHandler; use PHPUnit\Framework\TestCase; -use Symfony\Component\ErrorCatcher\Exception\FatalErrorException; -use Symfony\Component\ErrorCatcher\FatalErrorHandler\UndefinedFunctionFatalErrorHandler; +use Symfony\Component\Debug\Exception\FatalErrorException; +use Symfony\Component\Debug\FatalErrorHandler\UndefinedFunctionFatalErrorHandler; class UndefinedFunctionFatalErrorHandlerTest extends TestCase { @@ -25,7 +25,7 @@ class UndefinedFunctionFatalErrorHandlerTest extends TestCase $handler = new UndefinedFunctionFatalErrorHandler(); $exception = $handler->handleError($error, new FatalErrorException('', 0, $error['type'], $error['file'], $error['line'])); - $this->assertInstanceOf('Symfony\Component\ErrorCatcher\Exception\UndefinedFunctionException', $exception); + $this->assertInstanceOf('Symfony\Component\Debug\Exception\UndefinedFunctionException', $exception); // class names are case insensitive and PHP do not return the same $this->assertSame(strtolower($translatedMessage), strtolower($exception->getMessage())); $this->assertSame($error['type'], $exception->getSeverity()); @@ -43,7 +43,7 @@ class UndefinedFunctionFatalErrorHandlerTest extends TestCase 'file' => 'foo.php', 'message' => 'Call to undefined function test_namespaced_function()', ], - "Attempted to call function \"test_namespaced_function\" from the global namespace.\nDid you mean to call \"\\symfony\\component\\errorcatcher\\tests\\fatalerrorhandler\\test_namespaced_function\"?", + "Attempted to call function \"test_namespaced_function\" from the global namespace.\nDid you mean to call \"\\symfony\\component\\debug\\tests\\fatalerrorhandler\\test_namespaced_function\"?", ], [ [ @@ -52,7 +52,7 @@ class UndefinedFunctionFatalErrorHandlerTest extends TestCase 'file' => 'foo.php', 'message' => 'Call to undefined function Foo\\Bar\\Baz\\test_namespaced_function()', ], - "Attempted to call function \"test_namespaced_function\" from namespace \"Foo\\Bar\\Baz\".\nDid you mean to call \"\\symfony\\component\\errorcatcher\\tests\\fatalerrorhandler\\test_namespaced_function\"?", + "Attempted to call function \"test_namespaced_function\" from namespace \"Foo\\Bar\\Baz\".\nDid you mean to call \"\\symfony\\component\\debug\\tests\\fatalerrorhandler\\test_namespaced_function\"?", ], [ [ diff --git a/src/Symfony/Component/ErrorCatcher/Tests/FatalErrorHandler/UndefinedMethodFatalErrorHandlerTest.php b/src/Symfony/Component/Debug/Tests/FatalErrorHandler/UndefinedMethodFatalErrorHandlerTest.php similarity index 88% rename from src/Symfony/Component/ErrorCatcher/Tests/FatalErrorHandler/UndefinedMethodFatalErrorHandlerTest.php rename to src/Symfony/Component/Debug/Tests/FatalErrorHandler/UndefinedMethodFatalErrorHandlerTest.php index 0f0c9c5f2d..268a841351 100644 --- a/src/Symfony/Component/ErrorCatcher/Tests/FatalErrorHandler/UndefinedMethodFatalErrorHandlerTest.php +++ b/src/Symfony/Component/Debug/Tests/FatalErrorHandler/UndefinedMethodFatalErrorHandlerTest.php @@ -9,11 +9,11 @@ * file that was distributed with this source code. */ -namespace Symfony\Component\ErrorCatcher\Tests\FatalErrorHandler; +namespace Symfony\Component\Debug\Tests\FatalErrorHandler; use PHPUnit\Framework\TestCase; -use Symfony\Component\ErrorCatcher\Exception\FatalErrorException; -use Symfony\Component\ErrorCatcher\FatalErrorHandler\UndefinedMethodFatalErrorHandler; +use Symfony\Component\Debug\Exception\FatalErrorException; +use Symfony\Component\Debug\FatalErrorHandler\UndefinedMethodFatalErrorHandler; class UndefinedMethodFatalErrorHandlerTest extends TestCase { @@ -25,7 +25,7 @@ class UndefinedMethodFatalErrorHandlerTest extends TestCase $handler = new UndefinedMethodFatalErrorHandler(); $exception = $handler->handleError($error, new FatalErrorException('', 0, $error['type'], $error['file'], $error['line'])); - $this->assertInstanceOf('Symfony\Component\ErrorCatcher\Exception\UndefinedMethodException', $exception); + $this->assertInstanceOf('Symfony\Component\Debug\Exception\UndefinedMethodException', $exception); $this->assertSame($translatedMessage, $exception->getMessage()); $this->assertSame($error['type'], $exception->getSeverity()); $this->assertSame($error['file'], $exception->getFile()); diff --git a/src/Symfony/Component/ErrorCatcher/Tests/Fixtures/ErrorHandlerThatUsesThePreviousOne.php b/src/Symfony/Component/Debug/Tests/Fixtures/ErrorHandlerThatUsesThePreviousOne.php similarity index 88% rename from src/Symfony/Component/ErrorCatcher/Tests/Fixtures/ErrorHandlerThatUsesThePreviousOne.php rename to src/Symfony/Component/Debug/Tests/Fixtures/ErrorHandlerThatUsesThePreviousOne.php index c3c477fbf5..d449c40cc7 100644 --- a/src/Symfony/Component/ErrorCatcher/Tests/Fixtures/ErrorHandlerThatUsesThePreviousOne.php +++ b/src/Symfony/Component/Debug/Tests/Fixtures/ErrorHandlerThatUsesThePreviousOne.php @@ -1,6 +1,6 @@ --EXPECTF-- -object(Symfony\Component\ErrorCatcher\Exception\ClassNotFoundException)#%d (8) { +object(Symfony\Component\Debug\Exception\ClassNotFoundException)#%d (8) { ["message":protected]=> - string(138) "Attempted to load class "missing" from namespace "Symfony\Component\ErrorCatcher". + string(131) "Attempted to load class "missing" from namespace "Symfony\Component\Debug". Did you forget a "use" statement for another namespace?" ["string":"Exception":private]=> string(0) "" diff --git a/src/Symfony/Component/ErrorCatcher/Tests/phpt/exception_rethrown.phpt b/src/Symfony/Component/Debug/Tests/phpt/exception_rethrown.phpt similarity index 94% rename from src/Symfony/Component/ErrorCatcher/Tests/phpt/exception_rethrown.phpt rename to src/Symfony/Component/Debug/Tests/phpt/exception_rethrown.phpt index df3495c991..b743d93ad7 100644 --- a/src/Symfony/Component/ErrorCatcher/Tests/phpt/exception_rethrown.phpt +++ b/src/Symfony/Component/Debug/Tests/phpt/exception_rethrown.phpt @@ -3,7 +3,7 @@ Test rethrowing in custom exception handler --FILE-- string(37) "Error and exception handlers do match" } -object(Symfony\Component\ErrorCatcher\Exception\FatalErrorException)#%d (%d) { +object(Symfony\Component\Debug\Exception\FatalErrorException)#%d (%d) { ["message":protected]=> - string(186) "Error: Class Symfony\Component\ErrorCatcher\Broken contains 1 abstract method and must therefore be declared abstract or implement the remaining methods (JsonSerializable::jsonSerialize)" + string(179) "Error: Class Symfony\Component\Debug\Broken contains 1 abstract method and must therefore be declared abstract or implement the remaining methods (JsonSerializable::jsonSerialize)" %a } diff --git a/src/Symfony/Component/Debug/composer.json b/src/Symfony/Component/Debug/composer.json index 90f0347e6f..96fe201bdd 100644 --- a/src/Symfony/Component/Debug/composer.json +++ b/src/Symfony/Component/Debug/composer.json @@ -17,8 +17,7 @@ ], "require": { "php": "^7.1.3", - "psr/log": "~1.0", - "symfony/error-catcher": "^4.4|^5.0" + "psr/log": "~1.0" }, "conflict": { "symfony/http-kernel": "<3.4" diff --git a/src/Symfony/Component/ErrorCatcher/BufferingLogger.php b/src/Symfony/Component/ErrorCatcher/BufferingLogger.php deleted file mode 100644 index c0f1c56302..0000000000 --- a/src/Symfony/Component/ErrorCatcher/BufferingLogger.php +++ /dev/null @@ -1,37 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\ErrorCatcher; - -use Psr\Log\AbstractLogger; - -/** - * A buffering logger that stacks logs for later. - * - * @author Nicolas Grekas - */ -final class BufferingLogger extends AbstractLogger -{ - private $logs = []; - - public function log($level, $message, array $context = []) - { - $this->logs[] = [$level, $message, $context]; - } - - public function cleanLogs(): array - { - $logs = $this->logs; - $this->logs = []; - - return $logs; - } -} diff --git a/src/Symfony/Component/ErrorCatcher/ErrorHandler.php b/src/Symfony/Component/ErrorCatcher/ErrorHandler.php deleted file mode 100644 index 635482d42a..0000000000 --- a/src/Symfony/Component/ErrorCatcher/ErrorHandler.php +++ /dev/null @@ -1,711 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\ErrorCatcher; - -use Psr\Log\LoggerInterface; -use Psr\Log\LogLevel; -use Symfony\Component\Debug\DebugClassLoader; -use Symfony\Component\ErrorCatcher\Exception\FatalErrorException; -use Symfony\Component\ErrorCatcher\Exception\FatalThrowableError; -use Symfony\Component\ErrorCatcher\Exception\FlattenException; -use Symfony\Component\ErrorCatcher\Exception\OutOfMemoryException; -use Symfony\Component\ErrorCatcher\Exception\SilencedErrorContext; -use Symfony\Component\ErrorCatcher\FatalErrorHandler\ClassNotFoundFatalErrorHandler; -use Symfony\Component\ErrorCatcher\FatalErrorHandler\FatalErrorHandlerInterface; -use Symfony\Component\ErrorCatcher\FatalErrorHandler\UndefinedFunctionFatalErrorHandler; -use Symfony\Component\ErrorCatcher\FatalErrorHandler\UndefinedMethodFatalErrorHandler; - -/** - * A generic ErrorHandler for the PHP engine. - * - * Provides five bit fields that control how errors are handled: - * - 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 - * - screamedErrors: never @-silenced errors - * - * Each error level can be logged by a dedicated PSR-3 logger object. - * Screaming only applies to logging. - * Throwing takes precedence over logging. - * Uncaught exceptions are logged as E_ERROR. - * E_DEPRECATED and E_USER_DEPRECATED levels never throw. - * E_RECOVERABLE_ERROR and E_USER_ERROR levels always throw. - * Non catchable errors that can be detected at shutdown time are logged when the scream bit field allows so. - * As errors have a performance cost, repeated errors are all logged, so that the developer - * can see them and weight them as more important to fix than others of the same level. - * - * @author Nicolas Grekas - * @author Grégoire Pineau - * - * @final - */ -class ErrorHandler -{ - private $levels = [ - E_COMPILE_ERROR => 'Compile Error', - E_COMPILE_WARNING => 'Compile Warning', - E_CORE_ERROR => 'Core Error', - E_CORE_WARNING => 'Core Warning', - E_DEPRECATED => 'Deprecated', - E_ERROR => 'Error', - E_NOTICE => 'Notice', - E_PARSE => 'Parse Error', - E_RECOVERABLE_ERROR => 'Catchable Fatal Error', - E_STRICT => 'Runtime Notice', - E_USER_DEPRECATED => 'User Deprecated', - E_USER_ERROR => 'User Error', - E_USER_NOTICE => 'User Notice', - E_USER_WARNING => 'User Warning', - E_WARNING => 'Warning', - ]; - - private $loggers = [ - E_COMPILE_ERROR => [null, LogLevel::CRITICAL], - E_COMPILE_WARNING => [null, LogLevel::WARNING], - E_CORE_ERROR => [null, LogLevel::CRITICAL], - E_CORE_WARNING => [null, LogLevel::WARNING], - E_DEPRECATED => [null, LogLevel::INFO], - E_ERROR => [null, LogLevel::CRITICAL], - E_NOTICE => [null, LogLevel::WARNING], - E_PARSE => [null, LogLevel::CRITICAL], - E_RECOVERABLE_ERROR => [null, LogLevel::CRITICAL], - E_STRICT => [null, LogLevel::WARNING], - E_USER_DEPRECATED => [null, LogLevel::INFO], - E_USER_ERROR => [null, LogLevel::CRITICAL], - E_USER_NOTICE => [null, LogLevel::WARNING], - E_USER_WARNING => [null, LogLevel::WARNING], - E_WARNING => [null, LogLevel::WARNING], - ]; - - private $thrownErrors = 0x1FFF; // E_ALL - E_DEPRECATED - E_USER_DEPRECATED - private $scopedErrors = 0x1FFF; // E_ALL - E_DEPRECATED - E_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 $isRecursive = 0; - private $isRoot = false; - private $exceptionHandler; - private $bootstrappingLogger; - - private static $reservedMemory; - private static $toStringException = null; - private static $silencedErrorCache = []; - private static $silencedErrorCount = 0; - private static $exitCode = 0; - - /** - * Registers the error 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(self $handler = null, $replace = true) - { - if (null === self::$reservedMemory) { - self::$reservedMemory = str_repeat('x', 10240); - register_shutdown_function(__CLASS__.'::handleFatalError'); - } - - if ($handlerIsNew = null === $handler) { - $handler = new static(); - } - - if (null === $prev = set_error_handler([$handler, 'handleError'])) { - restore_error_handler(); - // Specifying the error types earlier would expose us to https://bugs.php.net/63206 - set_error_handler([$handler, 'handleError'], $handler->thrownErrors | $handler->loggedErrors); - $handler->isRoot = true; - } - - if ($handlerIsNew && \is_array($prev) && $prev[0] instanceof self) { - $handler = $prev[0]; - $replace = false; - } - if (!$replace && $prev) { - restore_error_handler(); - $handlerIsRegistered = \is_array($prev) && $handler === $prev[0]; - } else { - $handlerIsRegistered = true; - } - if (\is_array($prev = set_exception_handler([$handler, 'handleException'])) && $prev[0] instanceof self) { - restore_exception_handler(); - if (!$handlerIsRegistered) { - $handler = $prev[0]; - } elseif ($handler !== $prev[0] && $replace) { - set_exception_handler([$handler, 'handleException']); - $p = $prev[0]->setExceptionHandler(null); - $handler->setExceptionHandler($p); - $prev[0]->setExceptionHandler($p); - } - } else { - $handler->setExceptionHandler($prev); - } - - $handler->throwAt(E_ALL & $handler->thrownErrors, true); - - return $handler; - } - - public function __construct(BufferingLogger $bootstrappingLogger = null) - { - if ($bootstrappingLogger) { - $this->bootstrappingLogger = $bootstrappingLogger; - $this->setDefaultLogger($bootstrappingLogger); - } - $this->traceReflector = new \ReflectionProperty('Exception', 'trace'); - $this->traceReflector->setAccessible(true); - } - - /** - * Sets a logger to non assigned errors levels. - * - * @param LoggerInterface $logger A PSR-3 logger to put as default for the given levels - * @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 = E_ALL, $replace = false) - { - $loggers = []; - - if (\is_array($levels)) { - foreach ($levels as $type => $logLevel) { - if (empty($this->loggers[$type][0]) || $replace || $this->loggers[$type][0] === $this->bootstrappingLogger) { - $loggers[$type] = [$logger, $logLevel]; - } - } - } else { - if (null === $levels) { - $levels = E_ALL; - } - foreach ($this->loggers as $type => $log) { - if (($type & $levels) && (empty($log[0]) || $replace || $log[0] === $this->bootstrappingLogger)) { - $log[0] = $logger; - $loggers[$type] = $log; - } - } - } - - $this->setLoggers($loggers); - } - - /** - * Sets a logger for each error level. - * - * @param array $loggers Error levels to [LoggerInterface|null, LogLevel::*] map - * - * @return array The previous map - * - * @throws \InvalidArgumentException - */ - public function setLoggers(array $loggers) - { - $prevLogged = $this->loggedErrors; - $prev = $this->loggers; - $flush = []; - - foreach ($loggers as $type => $log) { - if (!isset($prev[$type])) { - throw new \InvalidArgumentException('Unknown error type: '.$type); - } - if (!\is_array($log)) { - $log = [$log]; - } elseif (!\array_key_exists(0, $log)) { - throw new \InvalidArgumentException('No logger provided'); - } - if (null === $log[0]) { - $this->loggedErrors &= ~$type; - } elseif ($log[0] instanceof LoggerInterface) { - $this->loggedErrors |= $type; - } else { - throw new \InvalidArgumentException('Invalid logger provided'); - } - $this->loggers[$type] = $log + $prev[$type]; - - if ($this->bootstrappingLogger && $prev[$type][0] === $this->bootstrappingLogger) { - $flush[$type] = $type; - } - } - $this->reRegister($prevLogged | $this->thrownErrors); - - if ($flush) { - foreach ($this->bootstrappingLogger->cleanLogs() as $log) { - $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]) { - $this->loggers[$type][0]->log($this->loggers[$type][1], $log[1], $log[2]); - } - } - } - - return $prev; - } - - /** - * Sets a user exception handler. - * - * @param callable $handler A handler that will be called on Exception - * - * @return callable|null The previous exception handler - */ - public function setExceptionHandler(callable $handler = null) - { - $prev = $this->exceptionHandler; - $this->exceptionHandler = $handler; - - return $prev; - } - - /** - * Sets the PHP error levels that throw an exception when a PHP error occurs. - * - * @param int $levels A bit field of E_* constants for thrown errors - * @param bool $replace Replace or amend the previous value - * - * @return int The previous value - */ - public function throwAt($levels, $replace = false) - { - $prev = $this->thrownErrors; - $this->thrownErrors = ($levels | E_RECOVERABLE_ERROR | E_USER_ERROR) & ~E_USER_DEPRECATED & ~E_DEPRECATED; - if (!$replace) { - $this->thrownErrors |= $prev; - } - $this->reRegister($prev | $this->loggedErrors); - - return $prev; - } - - /** - * Sets the PHP error levels for which local variables are preserved. - * - * @param int $levels A bit field of E_* constants for scoped errors - * @param bool $replace Replace or amend the previous value - * - * @return int The previous value - */ - public function scopeAt($levels, $replace = false) - { - $prev = $this->scopedErrors; - $this->scopedErrors = (int) $levels; - if (!$replace) { - $this->scopedErrors |= $prev; - } - - return $prev; - } - - /** - * Sets the PHP error levels for which the stack trace is preserved. - * - * @param int $levels A bit field of E_* constants for traced errors - * @param bool $replace Replace or amend the previous value - * - * @return int The previous value - */ - public function traceAt($levels, $replace = false) - { - $prev = $this->tracedErrors; - $this->tracedErrors = (int) $levels; - if (!$replace) { - $this->tracedErrors |= $prev; - } - - return $prev; - } - - /** - * Sets the error levels where the @-operator is ignored. - * - * @param int $levels A bit field of E_* constants for screamed errors - * @param bool $replace Replace or amend the previous value - * - * @return int The previous value - */ - public function screamAt($levels, $replace = false) - { - $prev = $this->screamedErrors; - $this->screamedErrors = (int) $levels; - if (!$replace) { - $this->screamedErrors |= $prev; - } - - return $prev; - } - - /** - * Re-registers as a PHP error handler if levels changed. - */ - private function reRegister($prev) - { - if ($prev !== $this->thrownErrors | $this->loggedErrors) { - $handler = set_error_handler('var_dump'); - $handler = \is_array($handler) ? $handler[0] : null; - restore_error_handler(); - if ($handler === $this) { - restore_error_handler(); - if ($this->isRoot) { - set_error_handler([$this, 'handleError'], $this->thrownErrors | $this->loggedErrors); - } else { - set_error_handler([$this, 'handleError']); - } - } - } - } - - /** - * Handles errors by filtering then logging them according to the configured bit fields. - * - * @return bool Returns false when no handling happens so that the PHP engine can handle the error itself - * - * @throws \ErrorException When $this->thrownErrors requests so - * - * @internal - */ - public function handleError(int $type, string $message, string $file, int $line): bool - { - // @deprecated to be removed in Symfony 5.0 - if (\PHP_VERSION_ID >= 70300 && $message && '"' === $message[0] && 0 === strpos($message, '"continue') && preg_match('/^"continue(?: \d++)?" targeting switch is equivalent to "break(?: \d++)?"\. Did you mean to use "continue(?: \d++)?"\?$/', $message)) { - $type = E_DEPRECATED; - } - - // Level is the current error reporting level to manage silent error. - $level = error_reporting(); - $silenced = 0 === ($level & $type); - // Strong errors are not authorized to be silenced. - $level |= E_RECOVERABLE_ERROR | E_USER_ERROR | E_DEPRECATED | E_USER_DEPRECATED; - $log = $this->loggedErrors & $type; - $throw = $this->thrownErrors & $type & $level; - $type &= $level | $this->screamedErrors; - - if (!$type || (!$log && !$throw)) { - return !$silenced && $type && $log; - } - $scope = $this->scopedErrors & $type; - - if (4 < $numArgs = \func_num_args()) { - $context = $scope ? (func_get_arg(4) ?: []) : []; - } else { - $context = []; - } - - if (isset($context['GLOBALS']) && $scope) { - $e = $context; // Whatever the signature of the method, - unset($e['GLOBALS'], $context); - $context = $e; - } - - if (false !== strpos($message, "class@anonymous\0")) { - $logMessage = $this->levels[$type].': '.(new FlattenException())->setMessage($message)->getMessage(); - } else { - $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, 5), $type, $file, $line, false) : []; - $errorAsException = new SilencedErrorContext($type, $file, $line, isset($lightTrace[1]) ? [$lightTrace[0]] : $lightTrace); - } elseif (isset(self::$silencedErrorCache[$id][$message])) { - $lightTrace = null; - $errorAsException = self::$silencedErrorCache[$id][$message]; - ++$errorAsException->count; - } else { - $lightTrace = []; - $errorAsException = null; - } - - if (100 < ++self::$silencedErrorCount) { - self::$silencedErrorCache = $lightTrace = []; - self::$silencedErrorCount = 1; - } - if ($errorAsException) { - self::$silencedErrorCache[$id][$message] = $errorAsException; - } - if (null === $lightTrace) { - return true; - } - } else { - $errorAsException = new \ErrorException($logMessage, 0, $type, $file, $line); - - if ($throw || $this->tracedErrors & $type) { - $backtrace = $errorAsException->getTrace(); - $lightTrace = $this->cleanTrace($backtrace, $type, $file, $line, $throw); - $this->traceReflector->setValue($errorAsException, $lightTrace); - } else { - $this->traceReflector->setValue($errorAsException, []); - $backtrace = []; - } - } - - if ($throw) { - if (E_USER_ERROR & $type) { - for ($i = 1; isset($backtrace[$i]); ++$i) { - if (isset($backtrace[$i]['function'], $backtrace[$i]['type'], $backtrace[$i - 1]['function']) - && '__toString' === $backtrace[$i]['function'] - && '->' === $backtrace[$i]['type'] - && !isset($backtrace[$i - 1]['class']) - && ('trigger_error' === $backtrace[$i - 1]['function'] || 'user_error' === $backtrace[$i - 1]['function']) - ) { - // Here, we know trigger_error() has been called from __toString(). - // PHP triggers a fatal error when throwing from __toString(). - // A small convention allows working around the limitation: - // given a caught $e exception in __toString(), quitting the method with - // `return trigger_error($e, E_USER_ERROR);` allows this error handler - // to make $e get through the __toString() barrier. - - foreach ($context as $e) { - if ($e instanceof \Throwable && $e->__toString() === $message) { - self::$toStringException = $e; - - return true; - } - } - - // Display the original error message instead of the default one. - $this->handleException($errorAsException); - - // Stop the process by giving back the error to the native handler. - return false; - } - } - } - - throw $errorAsException; - } - - if ($this->isRecursive) { - $log = 0; - } else { - if (!\defined('HHVM_VERSION')) { - $currentErrorHandler = set_error_handler('var_dump'); - restore_error_handler(); - } - - try { - $this->isRecursive = true; - $level = ($type & $level) ? $this->loggers[$type][1] : LogLevel::DEBUG; - $this->loggers[$type][0]->log($level, $logMessage, $errorAsException ? ['exception' => $errorAsException] : []); - } finally { - $this->isRecursive = false; - - if (!\defined('HHVM_VERSION')) { - set_error_handler($currentErrorHandler); - } - } - } - - return !$silenced && $type && $log; - } - - /** - * Handles an exception by logging then forwarding it to another handler. - * - * @param \Exception|\Throwable $exception An exception to handle - * @param array $error An array as returned by error_get_last() - * - * @internal - */ - public function handleException($exception, array $error = null) - { - if (null === $error) { - self::$exitCode = 255; - } - if (!$exception instanceof \Exception) { - $exception = new FatalThrowableError($exception); - } - $type = $exception instanceof FatalErrorException ? $exception->getSeverity() : E_ERROR; - $handlerException = null; - - if (($this->loggedErrors & $type) || $exception instanceof FatalThrowableError) { - if (false !== strpos($message = $exception->getMessage(), "class@anonymous\0")) { - $message = (new FlattenException())->setMessage($message)->getMessage(); - } - if ($exception instanceof FatalErrorException) { - if ($exception instanceof FatalThrowableError) { - $error = [ - 'type' => $type, - 'message' => $message, - 'file' => $exception->getFile(), - 'line' => $exception->getLine(), - ]; - } else { - $message = 'Fatal '.$message; - } - } elseif ($exception instanceof \ErrorException) { - $message = 'Uncaught '.$message; - } else { - $message = 'Uncaught Exception: '.$message; - } - } - if ($this->loggedErrors & $type) { - try { - $this->loggers[$type][0]->log($this->loggers[$type][1], $message, ['exception' => $exception]); - } catch (\Throwable $handlerException) { - } - } - if ($exception instanceof FatalErrorException && !$exception instanceof OutOfMemoryException && $error) { - foreach ($this->getFatalErrorHandlers() as $handler) { - if ($e = $handler->handleError($error, $exception)) { - $exception = $e; - break; - } - } - } - $exceptionHandler = $this->exceptionHandler; - $this->exceptionHandler = null; - try { - if (null !== $exceptionHandler) { - return $exceptionHandler($exception); - } - $handlerException = $handlerException ?: $exception; - } catch (\Throwable $handlerException) { - } - if ($exception === $handlerException) { - self::$reservedMemory = null; // Disable the fatal error handler - throw $exception; // Give back $exception to the native handler - } - $this->handleException($handlerException); - } - - /** - * Shutdown registered function for handling PHP fatal errors. - * - * @param array $error An array as returned by error_get_last() - * - * @internal - */ - public static function handleFatalError(array $error = null) - { - if (null === self::$reservedMemory) { - return; - } - - $handler = self::$reservedMemory = null; - $handlers = []; - $previousHandler = null; - $sameHandlerLimit = 10; - - while (!\is_array($handler) || !$handler[0] instanceof self) { - $handler = set_exception_handler('var_dump'); - restore_exception_handler(); - - 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 = []; - - if ($exit = null === $error) { - $error = error_get_last(); - } - - if ($error && $error['type'] &= E_PARSE | E_ERROR | E_CORE_ERROR | E_COMPILE_ERROR) { - // Let's not throw anymore but keep logging - $handler->throwAt(0, true); - $trace = isset($error['backtrace']) ? $error['backtrace'] : null; - - if (0 === strpos($error['message'], 'Allowed memory') || 0 === strpos($error['message'], 'Out of memory')) { - $exception = new OutOfMemoryException($handler->levels[$error['type']].': '.$error['message'], 0, $error['type'], $error['file'], $error['line'], 2, false, $trace); - } else { - $exception = new FatalErrorException($handler->levels[$error['type']].': '.$error['message'], 0, $error['type'], $error['file'], $error['line'], 2, true, $trace); - } - } else { - $exception = null; - } - - try { - if (null !== $exception) { - self::$exitCode = 255; - $handler->handleException($exception, $error); - } - } catch (FatalErrorException $e) { - // Ignore this re-throw - } - - if ($exit && self::$exitCode) { - $exitCode = self::$exitCode; - register_shutdown_function('register_shutdown_function', function () use ($exitCode) { exit($exitCode); }); - } - } - - /** - * Gets the fatal error handlers. - * - * Override this method if you want to define more fatal error handlers. - * - * @return FatalErrorHandlerInterface[] An array of FatalErrorHandlerInterface - */ - protected function getFatalErrorHandlers() - { - return [ - new UndefinedFunctionFatalErrorHandler(), - new UndefinedMethodFatalErrorHandler(), - new ClassNotFoundFatalErrorHandler(), - ]; - } - - /** - * Cleans the trace by removing function arguments and the frames added by the error handler and DebugClassLoader. - */ - private function cleanTrace($backtrace, $type, $file, $line, $throw) - { - $lightTrace = $backtrace; - - 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; - } - } - if (class_exists(DebugClassLoader::class, false)) { - for ($i = \count($lightTrace) - 2; 0 < $i; --$i) { - if (DebugClassLoader::class === ($lightTrace[$i]['class'] ?? null)) { - array_splice($lightTrace, --$i, 2); - } - } - } - if (!($throw || $this->scopedErrors & $type)) { - for ($i = 0; isset($lightTrace[$i]); ++$i) { - unset($lightTrace[$i]['args'], $lightTrace[$i]['object']); - } - } - - return $lightTrace; - } -} diff --git a/src/Symfony/Component/ErrorCatcher/Exception/ClassNotFoundException.php b/src/Symfony/Component/ErrorCatcher/Exception/ClassNotFoundException.php deleted file mode 100644 index f793f8d55c..0000000000 --- a/src/Symfony/Component/ErrorCatcher/Exception/ClassNotFoundException.php +++ /dev/null @@ -1,36 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\ErrorCatcher\Exception; - -/** - * Class (or Trait or Interface) Not Found Exception. - * - * @author Konstanton Myakshin - */ -class ClassNotFoundException extends FatalErrorException -{ - public function __construct(string $message, \ErrorException $previous) - { - parent::__construct( - $message, - $previous->getCode(), - $previous->getSeverity(), - $previous->getFile(), - $previous->getLine(), - null, - true, - null, - $previous->getPrevious() - ); - $this->setTrace($previous->getTrace()); - } -} diff --git a/src/Symfony/Component/ErrorCatcher/Exception/FatalErrorException.php b/src/Symfony/Component/ErrorCatcher/Exception/FatalErrorException.php deleted file mode 100644 index c98e46d5aa..0000000000 --- a/src/Symfony/Component/ErrorCatcher/Exception/FatalErrorException.php +++ /dev/null @@ -1,77 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\ErrorCatcher\Exception; - -/** - * Fatal Error Exception. - * - * @author Konstanton Myakshin - */ -class FatalErrorException extends \ErrorException -{ - public function __construct(string $message, int $code, int $severity, string $filename, int $lineno, int $traceOffset = null, bool $traceArgs = true, array $trace = null, \Throwable $previous = null) - { - parent::__construct($message, $code, $severity, $filename, $lineno, $previous); - - if (null !== $trace) { - if (!$traceArgs) { - foreach ($trace as &$frame) { - unset($frame['args'], $frame['this'], $frame); - } - } - - $this->setTrace($trace); - } elseif (null !== $traceOffset) { - if (\function_exists('xdebug_get_function_stack')) { - $trace = xdebug_get_function_stack(); - if (0 < $traceOffset) { - array_splice($trace, -$traceOffset); - } - - foreach ($trace as &$frame) { - if (!isset($frame['type'])) { - // XDebug pre 2.1.1 doesn't currently set the call type key http://bugs.xdebug.org/view.php?id=695 - if (isset($frame['class'])) { - $frame['type'] = '::'; - } - } elseif ('dynamic' === $frame['type']) { - $frame['type'] = '->'; - } elseif ('static' === $frame['type']) { - $frame['type'] = '::'; - } - - // XDebug also has a different name for the parameters array - if (!$traceArgs) { - unset($frame['params'], $frame['args']); - } elseif (isset($frame['params']) && !isset($frame['args'])) { - $frame['args'] = $frame['params']; - unset($frame['params']); - } - } - - unset($frame); - $trace = array_reverse($trace); - } else { - $trace = []; - } - - $this->setTrace($trace); - } - } - - protected function setTrace($trace) - { - $traceReflector = new \ReflectionProperty('Exception', 'trace'); - $traceReflector->setAccessible(true); - $traceReflector->setValue($this, $trace); - } -} diff --git a/src/Symfony/Component/ErrorCatcher/Exception/FatalThrowableError.php b/src/Symfony/Component/ErrorCatcher/Exception/FatalThrowableError.php deleted file mode 100644 index 86aa298db8..0000000000 --- a/src/Symfony/Component/ErrorCatcher/Exception/FatalThrowableError.php +++ /dev/null @@ -1,51 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\ErrorCatcher\Exception; - -/** - * Fatal Throwable Error. - * - * @author Nicolas Grekas - */ -class FatalThrowableError extends FatalErrorException -{ - private $originalClassName; - - public function __construct(\Throwable $e) - { - $this->originalClassName = \get_class($e); - - if ($e instanceof \ParseError) { - $severity = E_PARSE; - } elseif ($e instanceof \TypeError) { - $severity = E_RECOVERABLE_ERROR; - } else { - $severity = E_ERROR; - } - - \ErrorException::__construct( - $e->getMessage(), - $e->getCode(), - $severity, - $e->getFile(), - $e->getLine(), - $e->getPrevious() - ); - - $this->setTrace($e->getTrace()); - } - - public function getOriginalClassName(): string - { - return $this->originalClassName; - } -} diff --git a/src/Symfony/Component/ErrorCatcher/Exception/FlattenException.php b/src/Symfony/Component/ErrorCatcher/Exception/FlattenException.php index 8acff9dbfc..f783b6e620 100644 --- a/src/Symfony/Component/ErrorCatcher/Exception/FlattenException.php +++ b/src/Symfony/Component/ErrorCatcher/Exception/FlattenException.php @@ -11,6 +11,7 @@ namespace Symfony\Component\ErrorCatcher\Exception; +use Symfony\Component\Debug\Exception\FatalThrowableError; use Symfony\Component\HttpFoundation\Exception\RequestExceptionInterface; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface; diff --git a/src/Symfony/Component/ErrorCatcher/Exception/OutOfMemoryException.php b/src/Symfony/Component/ErrorCatcher/Exception/OutOfMemoryException.php deleted file mode 100644 index ed01e6e661..0000000000 --- a/src/Symfony/Component/ErrorCatcher/Exception/OutOfMemoryException.php +++ /dev/null @@ -1,21 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\ErrorCatcher\Exception; - -/** - * Out of memory exception. - * - * @author Nicolas Grekas - */ -class OutOfMemoryException extends FatalErrorException -{ -} diff --git a/src/Symfony/Component/ErrorCatcher/Exception/SilencedErrorContext.php b/src/Symfony/Component/ErrorCatcher/Exception/SilencedErrorContext.php deleted file mode 100644 index 46e9b55d7c..0000000000 --- a/src/Symfony/Component/ErrorCatcher/Exception/SilencedErrorContext.php +++ /dev/null @@ -1,67 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\ErrorCatcher\Exception; - -/** - * Data Object that represents a Silenced Error. - * - * @author Grégoire Pineau - */ -class SilencedErrorContext implements \JsonSerializable -{ - public $count = 1; - - private $severity; - private $file; - private $line; - private $trace; - - public function __construct(int $severity, string $file, int $line, array $trace = [], int $count = 1) - { - $this->severity = $severity; - $this->file = $file; - $this->line = $line; - $this->trace = $trace; - $this->count = $count; - } - - public function getSeverity() - { - return $this->severity; - } - - public function getFile() - { - return $this->file; - } - - public function getLine() - { - return $this->line; - } - - public function getTrace() - { - return $this->trace; - } - - public function JsonSerialize() - { - return [ - 'severity' => $this->severity, - 'file' => $this->file, - 'line' => $this->line, - 'trace' => $this->trace, - 'count' => $this->count, - ]; - } -} diff --git a/src/Symfony/Component/ErrorCatcher/Exception/UndefinedFunctionException.php b/src/Symfony/Component/ErrorCatcher/Exception/UndefinedFunctionException.php deleted file mode 100644 index aef70895c1..0000000000 --- a/src/Symfony/Component/ErrorCatcher/Exception/UndefinedFunctionException.php +++ /dev/null @@ -1,36 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\ErrorCatcher\Exception; - -/** - * Undefined Function Exception. - * - * @author Konstanton Myakshin - */ -class UndefinedFunctionException extends FatalErrorException -{ - public function __construct(string $message, \ErrorException $previous) - { - parent::__construct( - $message, - $previous->getCode(), - $previous->getSeverity(), - $previous->getFile(), - $previous->getLine(), - null, - true, - null, - $previous->getPrevious() - ); - $this->setTrace($previous->getTrace()); - } -} diff --git a/src/Symfony/Component/ErrorCatcher/Exception/UndefinedMethodException.php b/src/Symfony/Component/ErrorCatcher/Exception/UndefinedMethodException.php deleted file mode 100644 index 50a9d1f391..0000000000 --- a/src/Symfony/Component/ErrorCatcher/Exception/UndefinedMethodException.php +++ /dev/null @@ -1,36 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\ErrorCatcher\Exception; - -/** - * Undefined Method Exception. - * - * @author Grégoire Pineau - */ -class UndefinedMethodException extends FatalErrorException -{ - public function __construct(string $message, \ErrorException $previous) - { - parent::__construct( - $message, - $previous->getCode(), - $previous->getSeverity(), - $previous->getFile(), - $previous->getLine(), - null, - true, - null, - $previous->getPrevious() - ); - $this->setTrace($previous->getTrace()); - } -} diff --git a/src/Symfony/Component/ErrorCatcher/ExceptionHandler.php b/src/Symfony/Component/ErrorCatcher/ExceptionHandler.php deleted file mode 100644 index 225db00622..0000000000 --- a/src/Symfony/Component/ErrorCatcher/ExceptionHandler.php +++ /dev/null @@ -1,177 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\ErrorCatcher; - -use Symfony\Component\ErrorCatcher\ErrorRenderer\HtmlErrorRenderer; -use Symfony\Component\ErrorCatcher\Exception\FlattenException; -use Symfony\Component\ErrorCatcher\Exception\OutOfMemoryException; -use Symfony\Component\HttpKernel\Debug\FileLinkFormatter; - -/** - * ExceptionHandler converts an exception to a Response object. - * - * It is mostly useful in debug mode to replace the default PHP/XDebug - * output with something prettier and more useful. - * - * As this class is mainly used during Kernel boot, where nothing is yet - * available, the Response content is always HTML. - * - * @author Fabien Potencier - * @author Nicolas Grekas - * - * @final - */ -class ExceptionHandler -{ - private $charset; - private $errorRenderer; - private $handler; - private $caughtBuffer; - private $caughtLength; - - /** - * Registers the exception handler. - * - * @param bool $debug Enable/disable debug mode, where the stack trace is displayed - * @param string|null $charset The charset used by exception messages - * @param string|null $fileLinkFormat The IDE link template - * - * @return static - */ - public static function register($debug = true, $charset = null, $fileLinkFormat = null) - { - $handler = new static($debug, $charset, $fileLinkFormat); - - $prev = set_exception_handler([$handler, 'handle']); - if (\is_array($prev) && $prev[0] instanceof ErrorHandler) { - restore_exception_handler(); - $prev[0]->setExceptionHandler([$handler, 'handle']); - } - - return $handler; - } - - public function __construct(bool $debug = true, string $charset = null, $fileLinkFormat = null, HtmlErrorRenderer $errorRenderer = null) - { - $this->charset = $charset ?: ini_get('default_charset') ?: 'UTF-8'; - $this->errorRenderer = $errorRenderer ?? new HtmlErrorRenderer($debug, $this->charset, $fileLinkFormat); - } - - /** - * Sets the format for links to source files. - * - * @param string|FileLinkFormatter $fileLinkFormat The format for links to source files - * - * @return string The previous file link format - */ - public function setFileLinkFormat($fileLinkFormat) - { - return $this->errorRenderer->setFileLinkFormat($fileLinkFormat); - } - - /** - * Sets a user exception handler. - * - * @param callable $handler An handler that will be called on Exception - * - * @return callable|null The previous exception handler if any - */ - public function setHandler(callable $handler = null) - { - $old = $this->handler; - $this->handler = $handler; - - return $old; - } - - /** - * Sends a response for the given Exception. - * - * To be as fail-safe as possible, the exception is first handled - * by our simple exception handler, then by the user exception handler. - * The latter takes precedence and any output from the former is cancelled, - * if and only if nothing bad happens in this handling path. - */ - public function handle(\Exception $exception) - { - if (null === $this->handler || $exception instanceof OutOfMemoryException) { - $this->sendPhpResponse($exception); - - return; - } - - $caughtLength = $this->caughtLength = 0; - - ob_start(function ($buffer) { - $this->caughtBuffer = $buffer; - - return ''; - }); - - $this->sendPhpResponse($exception); - while (null === $this->caughtBuffer && ob_end_flush()) { - // Empty loop, everything is in the condition - } - if (isset($this->caughtBuffer[0])) { - ob_start(function ($buffer) { - if ($this->caughtLength) { - // use substr_replace() instead of substr() for mbstring overloading resistance - $cleanBuffer = substr_replace($buffer, '', 0, $this->caughtLength); - if (isset($cleanBuffer[0])) { - $buffer = $cleanBuffer; - } - } - - return $buffer; - }); - - echo $this->caughtBuffer; - $caughtLength = ob_get_length(); - } - $this->caughtBuffer = null; - - try { - ($this->handler)($exception); - $this->caughtLength = $caughtLength; - } catch (\Exception $e) { - if (!$caughtLength) { - // All handlers failed. Let PHP handle that now. - throw $exception; - } - } - } - - /** - * Sends the error associated with the given Exception as a plain PHP response. - * - * This method uses plain PHP functions like header() and echo to output - * the response. - * - * @param \Throwable|FlattenException $exception A \Throwable or FlattenException instance - */ - public function sendPhpResponse($exception) - { - if ($exception instanceof \Throwable) { - $exception = FlattenException::createFromThrowable($exception); - } - - if (!headers_sent()) { - header(sprintf('HTTP/1.0 %s', $exception->getStatusCode())); - foreach ($exception->getHeaders() as $name => $value) { - header($name.': '.$value, false); - } - header('Content-Type: text/html; charset='.$this->charset); - } - - echo $this->errorRenderer->render($exception); - } -} diff --git a/src/Symfony/Component/ErrorCatcher/FatalErrorHandler/ClassNotFoundFatalErrorHandler.php b/src/Symfony/Component/ErrorCatcher/FatalErrorHandler/ClassNotFoundFatalErrorHandler.php deleted file mode 100644 index a9f3e39bb7..0000000000 --- a/src/Symfony/Component/ErrorCatcher/FatalErrorHandler/ClassNotFoundFatalErrorHandler.php +++ /dev/null @@ -1,193 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\ErrorCatcher\FatalErrorHandler; - -use Composer\Autoload\ClassLoader as ComposerClassLoader; -use Symfony\Component\ClassLoader\ClassLoader as SymfonyClassLoader; -use Symfony\Component\Debug\DebugClassLoader; -use Symfony\Component\ErrorCatcher\Exception\ClassNotFoundException; -use Symfony\Component\ErrorCatcher\Exception\FatalErrorException; - -/** - * ErrorHandler for classes that do not exist. - * - * @author Fabien Potencier - */ -class ClassNotFoundFatalErrorHandler implements FatalErrorHandlerInterface -{ - /** - * {@inheritdoc} - */ - public function handleError(array $error, FatalErrorException $exception) - { - $messageLen = \strlen($error['message']); - $notFoundSuffix = '\' not found'; - $notFoundSuffixLen = \strlen($notFoundSuffix); - if ($notFoundSuffixLen > $messageLen) { - return; - } - - if (0 !== substr_compare($error['message'], $notFoundSuffix, -$notFoundSuffixLen)) { - return; - } - - foreach (['class', 'interface', 'trait'] as $typeName) { - $prefix = ucfirst($typeName).' \''; - $prefixLen = \strlen($prefix); - if (0 !== strpos($error['message'], $prefix)) { - continue; - } - - $fullyQualifiedClassName = substr($error['message'], $prefixLen, -$notFoundSuffixLen); - if (false !== $namespaceSeparatorIndex = strrpos($fullyQualifiedClassName, '\\')) { - $className = substr($fullyQualifiedClassName, $namespaceSeparatorIndex + 1); - $namespacePrefix = substr($fullyQualifiedClassName, 0, $namespaceSeparatorIndex); - $message = sprintf('Attempted to load %s "%s" from namespace "%s".', $typeName, $className, $namespacePrefix); - $tail = ' for another namespace?'; - } else { - $className = $fullyQualifiedClassName; - $message = sprintf('Attempted to load %s "%s" from the global namespace.', $typeName, $className); - $tail = '?'; - } - - if ($candidates = $this->getClassCandidates($className)) { - $tail = array_pop($candidates).'"?'; - if ($candidates) { - $tail = ' for e.g. "'.implode('", "', $candidates).'" or "'.$tail; - } else { - $tail = ' for "'.$tail; - } - } - $message .= "\nDid you forget a \"use\" statement".$tail; - - return new ClassNotFoundException($message, $exception); - } - } - - /** - * Tries to guess the full namespace for a given class name. - * - * By default, it looks for PSR-0 and PSR-4 classes registered via a Symfony or a Composer - * autoloader (that should cover all common cases). - * - * @param string $class A class name (without its namespace) - * - * @return array An array of possible fully qualified class names - */ - private function getClassCandidates(string $class): array - { - if (!\is_array($functions = spl_autoload_functions())) { - return []; - } - - // find Symfony and Composer autoloaders - $classes = []; - - foreach ($functions as $function) { - if (!\is_array($function)) { - continue; - } - // get class loaders wrapped by DebugClassLoader - if ($function[0] instanceof DebugClassLoader) { - $function = $function[0]->getClassLoader(); - - if (!\is_array($function)) { - continue; - } - } - - if ($function[0] instanceof ComposerClassLoader || $function[0] instanceof SymfonyClassLoader) { - foreach ($function[0]->getPrefixes() as $prefix => $paths) { - foreach ($paths as $path) { - $classes = array_merge($classes, $this->findClassInPath($path, $class, $prefix)); - } - } - } - if ($function[0] instanceof ComposerClassLoader) { - foreach ($function[0]->getPrefixesPsr4() as $prefix => $paths) { - foreach ($paths as $path) { - $classes = array_merge($classes, $this->findClassInPath($path, $class, $prefix)); - } - } - } - } - - return array_unique($classes); - } - - private function findClassInPath(string $path, string $class, string $prefix): array - { - if (!$path = realpath($path.'/'.strtr($prefix, '\\_', '//')) ?: realpath($path.'/'.\dirname(strtr($prefix, '\\_', '//'))) ?: realpath($path)) { - return []; - } - - $classes = []; - $filename = $class.'.php'; - foreach (new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($path, \RecursiveDirectoryIterator::SKIP_DOTS), \RecursiveIteratorIterator::LEAVES_ONLY) as $file) { - if ($filename == $file->getFileName() && $class = $this->convertFileToClass($path, $file->getPathName(), $prefix)) { - $classes[] = $class; - } - } - - return $classes; - } - - private function convertFileToClass(string $path, string $file, string $prefix): ?string - { - $candidates = [ - // namespaced class - $namespacedClass = str_replace([$path.\DIRECTORY_SEPARATOR, '.php', '/'], ['', '', '\\'], $file), - // namespaced class (with target dir) - $prefix.$namespacedClass, - // namespaced class (with target dir and separator) - $prefix.'\\'.$namespacedClass, - // PEAR class - str_replace('\\', '_', $namespacedClass), - // PEAR class (with target dir) - str_replace('\\', '_', $prefix.$namespacedClass), - // PEAR class (with target dir and separator) - str_replace('\\', '_', $prefix.'\\'.$namespacedClass), - ]; - - if ($prefix) { - $candidates = array_filter($candidates, function ($candidate) use ($prefix) { return 0 === strpos($candidate, $prefix); }); - } - - // We cannot use the autoloader here as most of them use require; but if the class - // is not found, the new autoloader call will require the file again leading to a - // "cannot redeclare class" error. - foreach ($candidates as $candidate) { - if ($this->classExists($candidate)) { - return $candidate; - } - } - - try { - require_once $file; - } catch (\Throwable $e) { - return null; - } - - foreach ($candidates as $candidate) { - if ($this->classExists($candidate)) { - return $candidate; - } - } - - return null; - } - - private function classExists(string $class): bool - { - return class_exists($class, false) || interface_exists($class, false) || trait_exists($class, false); - } -} diff --git a/src/Symfony/Component/ErrorCatcher/FatalErrorHandler/FatalErrorHandlerInterface.php b/src/Symfony/Component/ErrorCatcher/FatalErrorHandler/FatalErrorHandlerInterface.php deleted file mode 100644 index 6fc237c094..0000000000 --- a/src/Symfony/Component/ErrorCatcher/FatalErrorHandler/FatalErrorHandlerInterface.php +++ /dev/null @@ -1,32 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\ErrorCatcher\FatalErrorHandler; - -use Symfony\Component\ErrorCatcher\Exception\FatalErrorException; - -/** - * Attempts to convert fatal errors to exceptions. - * - * @author Fabien Potencier - */ -interface FatalErrorHandlerInterface -{ - /** - * Attempts to convert an error into an exception. - * - * @param array $error An array as returned by error_get_last() - * @param FatalErrorException $exception A FatalErrorException instance - * - * @return FatalErrorException|null A FatalErrorException instance if the class is able to convert the error, null otherwise - */ - public function handleError(array $error, FatalErrorException $exception); -} diff --git a/src/Symfony/Component/ErrorCatcher/FatalErrorHandler/UndefinedFunctionFatalErrorHandler.php b/src/Symfony/Component/ErrorCatcher/FatalErrorHandler/UndefinedFunctionFatalErrorHandler.php deleted file mode 100644 index f20cb1185d..0000000000 --- a/src/Symfony/Component/ErrorCatcher/FatalErrorHandler/UndefinedFunctionFatalErrorHandler.php +++ /dev/null @@ -1,84 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\ErrorCatcher\FatalErrorHandler; - -use Symfony\Component\ErrorCatcher\Exception\FatalErrorException; -use Symfony\Component\ErrorCatcher\Exception\UndefinedFunctionException; - -/** - * ErrorHandler for undefined functions. - * - * @author Fabien Potencier - */ -class UndefinedFunctionFatalErrorHandler implements FatalErrorHandlerInterface -{ - /** - * {@inheritdoc} - */ - public function handleError(array $error, FatalErrorException $exception) - { - $messageLen = \strlen($error['message']); - $notFoundSuffix = '()'; - $notFoundSuffixLen = \strlen($notFoundSuffix); - if ($notFoundSuffixLen > $messageLen) { - return; - } - - if (0 !== substr_compare($error['message'], $notFoundSuffix, -$notFoundSuffixLen)) { - return; - } - - $prefix = 'Call to undefined function '; - $prefixLen = \strlen($prefix); - if (0 !== strpos($error['message'], $prefix)) { - return; - } - - $fullyQualifiedFunctionName = substr($error['message'], $prefixLen, -$notFoundSuffixLen); - if (false !== $namespaceSeparatorIndex = strrpos($fullyQualifiedFunctionName, '\\')) { - $functionName = substr($fullyQualifiedFunctionName, $namespaceSeparatorIndex + 1); - $namespacePrefix = substr($fullyQualifiedFunctionName, 0, $namespaceSeparatorIndex); - $message = sprintf('Attempted to call function "%s" from namespace "%s".', $functionName, $namespacePrefix); - } else { - $functionName = $fullyQualifiedFunctionName; - $message = sprintf('Attempted to call function "%s" from the global namespace.', $functionName); - } - - $candidates = []; - foreach (get_defined_functions() as $type => $definedFunctionNames) { - foreach ($definedFunctionNames as $definedFunctionName) { - if (false !== $namespaceSeparatorIndex = strrpos($definedFunctionName, '\\')) { - $definedFunctionNameBasename = substr($definedFunctionName, $namespaceSeparatorIndex + 1); - } else { - $definedFunctionNameBasename = $definedFunctionName; - } - - if ($definedFunctionNameBasename === $functionName) { - $candidates[] = '\\'.$definedFunctionName; - } - } - } - - if ($candidates) { - sort($candidates); - $last = array_pop($candidates).'"?'; - if ($candidates) { - $candidates = 'e.g. "'.implode('", "', $candidates).'" or "'.$last; - } else { - $candidates = '"'.$last; - } - $message .= "\nDid you mean to call ".$candidates; - } - - return new UndefinedFunctionException($message, $exception); - } -} diff --git a/src/Symfony/Component/ErrorCatcher/FatalErrorHandler/UndefinedMethodFatalErrorHandler.php b/src/Symfony/Component/ErrorCatcher/FatalErrorHandler/UndefinedMethodFatalErrorHandler.php deleted file mode 100644 index 6f9a3eb545..0000000000 --- a/src/Symfony/Component/ErrorCatcher/FatalErrorHandler/UndefinedMethodFatalErrorHandler.php +++ /dev/null @@ -1,66 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\ErrorCatcher\FatalErrorHandler; - -use Symfony\Component\ErrorCatcher\Exception\FatalErrorException; -use Symfony\Component\ErrorCatcher\Exception\UndefinedMethodException; - -/** - * ErrorHandler for undefined methods. - * - * @author Grégoire Pineau - */ -class UndefinedMethodFatalErrorHandler implements FatalErrorHandlerInterface -{ - /** - * {@inheritdoc} - */ - public function handleError(array $error, FatalErrorException $exception) - { - preg_match('/^Call to undefined method (.*)::(.*)\(\)$/', $error['message'], $matches); - if (!$matches) { - return; - } - - $className = $matches[1]; - $methodName = $matches[2]; - - $message = sprintf('Attempted to call an undefined method named "%s" of class "%s".', $methodName, $className); - - if (!class_exists($className) || null === $methods = get_class_methods($className)) { - // failed to get the class or its methods on which an unknown method was called (for example on an anonymous class) - return new UndefinedMethodException($message, $exception); - } - - $candidates = []; - foreach ($methods as $definedMethodName) { - $lev = levenshtein($methodName, $definedMethodName); - if ($lev <= \strlen($methodName) / 3 || false !== strpos($definedMethodName, $methodName)) { - $candidates[] = $definedMethodName; - } - } - - if ($candidates) { - sort($candidates); - $last = array_pop($candidates).'"?'; - if ($candidates) { - $candidates = 'e.g. "'.implode('", "', $candidates).'" or "'.$last; - } else { - $candidates = '"'.$last; - } - - $message .= "\nDid you mean to call ".$candidates; - } - - return new UndefinedMethodException($message, $exception); - } -} diff --git a/src/Symfony/Component/ErrorCatcher/Tests/Exception/FlattenExceptionTest.php b/src/Symfony/Component/ErrorCatcher/Tests/Exception/FlattenExceptionTest.php index 3f8749578b..6a39025cc1 100644 --- a/src/Symfony/Component/ErrorCatcher/Tests/Exception/FlattenExceptionTest.php +++ b/src/Symfony/Component/ErrorCatcher/Tests/Exception/FlattenExceptionTest.php @@ -12,7 +12,7 @@ namespace Symfony\Component\ErrorCatcher\Tests\Exception; use PHPUnit\Framework\TestCase; -use Symfony\Component\ErrorCatcher\Exception\FatalThrowableError; +use Symfony\Component\Debug\Exception\FatalThrowableError; use Symfony\Component\ErrorCatcher\Exception\FlattenException; use Symfony\Component\HttpFoundation\Exception\SuspiciousOperationException; use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; diff --git a/src/Symfony/Component/ErrorCatcher/Tests/Fixtures/LoggerThatSetAnErrorHandler.php b/src/Symfony/Component/ErrorCatcher/Tests/Fixtures/LoggerThatSetAnErrorHandler.php deleted file mode 100644 index 380d4b3dd6..0000000000 --- a/src/Symfony/Component/ErrorCatcher/Tests/Fixtures/LoggerThatSetAnErrorHandler.php +++ /dev/null @@ -1,25 +0,0 @@ -logs[] = [$level, $message, $context]; - restore_error_handler(); - } - - public function cleanLogs(): array - { - $logs = $this->logs; - $this->logs = []; - - return $logs; - } -} diff --git a/src/Symfony/Component/ErrorCatcher/Tests/Fixtures/PEARClass.php b/src/Symfony/Component/ErrorCatcher/Tests/Fixtures/PEARClass.php deleted file mode 100644 index 4cc8c825dc..0000000000 --- a/src/Symfony/Component/ErrorCatcher/Tests/Fixtures/PEARClass.php +++ /dev/null @@ -1,5 +0,0 @@ -