diff --git a/src/Symfony/Bundle/FrameworkBundle/Console/Application.php b/src/Symfony/Bundle/FrameworkBundle/Console/Application.php index 0fdb7ecd44..08595ce917 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Console/Application.php +++ b/src/Symfony/Bundle/FrameworkBundle/Console/Application.php @@ -174,10 +174,8 @@ class Application extends BaseApplication if ($bundle instanceof Bundle) { try { $bundle->registerCommands($this); - } catch (\Exception $e) { - $this->registrationErrors[] = $e; } catch (\Throwable $e) { - $this->registrationErrors[] = new FatalThrowableError($e); + $this->registrationErrors[] = $e; } } } @@ -192,10 +190,8 @@ class Application extends BaseApplication if (!isset($lazyCommandIds[$id])) { try { $this->add($container->get($id)); - } catch (\Exception $e) { - $this->registrationErrors[] = $e; } catch (\Throwable $e) { - $this->registrationErrors[] = new FatalThrowableError($e); + $this->registrationErrors[] = $e; } } } @@ -211,6 +207,10 @@ class Application extends BaseApplication (new SymfonyStyle($input, $output))->warning('Some commands could not be registered:'); foreach ($this->registrationErrors as $error) { + if (!$error instanceof \Exception) { + $error = new FatalThrowableError($error); + } + $this->doRenderException($error, $output); } } diff --git a/src/Symfony/Component/Console/Application.php b/src/Symfony/Component/Console/Application.php index 795c7a5df5..b7eb2f8210 100644 --- a/src/Symfony/Component/Console/Application.php +++ b/src/Symfony/Component/Console/Application.php @@ -127,7 +127,7 @@ class Application implements ResetInterface $output = new ConsoleOutput(); } - $renderException = function ($e) use ($output) { + $renderException = function (\Throwable $e) use ($output) { if (!$e instanceof \Exception) { $e = class_exists(FatalThrowableError::class) ? new FatalThrowableError($e) : (class_exists(LegacyFatalThrowableError::class) ? new LegacyFatalThrowableError($e) : new \ErrorException($e->getMessage(), $e->getCode(), E_ERROR, $e->getFile(), $e->getLine())); } diff --git a/src/Symfony/Component/ErrorHandler/ErrorHandler.php b/src/Symfony/Component/ErrorHandler/ErrorHandler.php index f81700b5ad..37047911f7 100644 --- a/src/Symfony/Component/ErrorHandler/ErrorHandler.php +++ b/src/Symfony/Component/ErrorHandler/ErrorHandler.php @@ -14,7 +14,6 @@ namespace Symfony\Component\ErrorHandler; use Psr\Log\LoggerInterface; use Psr\Log\LogLevel; use Symfony\Component\ErrorHandler\Exception\FatalErrorException; -use Symfony\Component\ErrorHandler\Exception\FatalThrowableError; use Symfony\Component\ErrorHandler\Exception\OutOfMemoryException; use Symfony\Component\ErrorHandler\Exception\SilencedErrorContext; use Symfony\Component\ErrorHandler\FatalErrorHandler\ClassNotFoundFatalErrorHandler; @@ -266,7 +265,7 @@ class ErrorHandler if ($flush) { foreach ($this->bootstrappingLogger->cleanLogs() as $log) { - $type = $log[2]['exception'] instanceof \ErrorException ? $log[2]['exception']->getSeverity() : E_ERROR; + $type = ThrowableUtils::getSeverity($log[2]['exception']); if (!isset($flush[$type])) { $this->bootstrappingLogger->log($log[0], $log[1], $log[2]); } elseif ($this->loggers[$type][0]) { @@ -281,7 +280,7 @@ class ErrorHandler /** * Sets a user exception handler. * - * @param callable|null $handler A handler that will be called on Exception + * @param callable|null $handler A handler that must support \Throwable instances that will be called on Exception * * @return callable|null The previous exception handler */ @@ -539,57 +538,64 @@ class ErrorHandler /** * 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() + * @param array $error An array as returned by error_get_last() * * @internal */ - public function handleException($exception, array $error = null) + public function handleException(\Throwable $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; + + $type = ThrowableUtils::getSeverity($exception); $handlerException = null; - if (($this->loggedErrors & $type) || $exception instanceof FatalThrowableError) { + if (($this->loggedErrors & $type) || $exception instanceof \Error) { if (false !== strpos($message = $exception->getMessage(), "class@anonymous\0")) { $message = $this->parseAnonymousClass($message); } + if ($exception instanceof FatalErrorException) { - if ($exception instanceof FatalThrowableError) { - $error = [ - 'type' => $type, - 'message' => $message, - 'file' => $exception->getFile(), - 'line' => $exception->getLine(), - ]; - } else { - $message = 'Fatal '.$message; - } + $message = 'Fatal '.$message; } elseif ($exception instanceof \ErrorException) { $message = 'Uncaught '.$message; + } elseif ($exception instanceof \Error) { + $error = [ + 'type' => $type, + 'message' => $message, + 'file' => $exception->getFile(), + 'line' => $exception->getLine(), + ]; + $message = 'Uncaught Error: '.$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) { } } + + // temporary until fatal error handlers rework + $originalException = $exception; + if (!$exception instanceof \Exception) { + $exception = new FatalErrorException($exception->getMessage(), $exception->getCode(), $type, $exception->getFile(), $exception->getLine(), null, true, $exception->getTrace()); + } + if ($exception instanceof FatalErrorException && !$exception instanceof OutOfMemoryException && $error) { foreach ($this->getFatalErrorHandlers() as $handler) { if ($e = $handler->handleError($error, $exception)) { - $exception = $e; + $convertedException = $e; break; } } } + + $exception = $convertedException ?? $originalException; $exceptionHandler = $this->exceptionHandler; if ((!\is_array($exceptionHandler) || !$exceptionHandler[0] instanceof self || 'sendPhpResponse' !== $exceptionHandler[1]) && !\in_array(\PHP_SAPI, ['cli', 'phpdbg'], true)) { $this->exceptionHandler = [$this, 'sendPhpResponse']; diff --git a/src/Symfony/Component/ErrorHandler/Exception/FatalThrowableError.php b/src/Symfony/Component/ErrorHandler/Exception/FatalThrowableError.php index a690c83597..460d3e1ae8 100644 --- a/src/Symfony/Component/ErrorHandler/Exception/FatalThrowableError.php +++ b/src/Symfony/Component/ErrorHandler/Exception/FatalThrowableError.php @@ -11,6 +11,8 @@ namespace Symfony\Component\ErrorHandler\Exception; +use Symfony\Component\ErrorHandler\ThrowableUtils; + /** * Fatal Throwable Error. * @@ -24,18 +26,10 @@ class FatalThrowableError extends FatalErrorException { $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, + ThrowableUtils::getSeverity($e), $e->getFile(), $e->getLine(), $e->getPrevious() diff --git a/src/Symfony/Component/ErrorHandler/Tests/ErrorHandlerTest.php b/src/Symfony/Component/ErrorHandler/Tests/ErrorHandlerTest.php index e6374b8b0b..bb694cfc3b 100644 --- a/src/Symfony/Component/ErrorHandler/Tests/ErrorHandlerTest.php +++ b/src/Symfony/Component/ErrorHandler/Tests/ErrorHandlerTest.php @@ -382,18 +382,19 @@ class ErrorHandlerTest extends TestCase restore_error_handler(); } - public function testHandleException() + /** + * @dataProvider handleExceptionProvider + */ + public function testHandleException(string $expectedMessage, \Throwable $exception) { try { $logger = $this->getMockBuilder('Psr\Log\LoggerInterface')->getMock(); $handler = ErrorHandler::register(); - $exception = new \Exception('foo'); - - $logArgCheck = function ($level, $message, $context) { - $this->assertSame('Uncaught Exception: foo', $message); + $logArgCheck = function ($level, $message, $context) use ($expectedMessage, $exception) { + $this->assertSame($expectedMessage, $message); $this->assertArrayHasKey('exception', $context); - $this->assertInstanceOf(\Exception::class, $context['exception']); + $this->assertInstanceOf(\get_class($exception), $context['exception']); }; $logger @@ -407,7 +408,7 @@ class ErrorHandlerTest extends TestCase try { $handler->handleException($exception); $this->fail('Exception expected'); - } catch (\Exception $e) { + } catch (\Throwable $e) { $this->assertSame($exception, $e); } @@ -422,6 +423,15 @@ class ErrorHandlerTest extends TestCase } } + public function handleExceptionProvider(): array + { + return [ + ['Uncaught Exception: foo', new \Exception('foo')], + ['Uncaught Error: bar', new \Error('bar')], + ['Uncaught ccc', new \ErrorException('ccc')], + ]; + } + public function testBootstrappingLogger() { $bootLogger = new BufferingLogger(); diff --git a/src/Symfony/Component/ErrorHandler/ThrowableUtils.php b/src/Symfony/Component/ErrorHandler/ThrowableUtils.php new file mode 100644 index 0000000000..5cbe87f493 --- /dev/null +++ b/src/Symfony/Component/ErrorHandler/ThrowableUtils.php @@ -0,0 +1,35 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\ErrorHandler; + +/** + * @internal + */ +class ThrowableUtils +{ + public static function getSeverity(\Throwable $throwable): int + { + if ($throwable instanceof \ErrorException) { + return $throwable->getSeverity(); + } + + if ($throwable instanceof \ParseError) { + return E_PARSE; + } + + if ($throwable instanceof \TypeError) { + return E_RECOVERABLE_ERROR; + } + + return E_ERROR; + } +} diff --git a/src/Symfony/Component/HttpKernel/EventListener/DebugHandlersListener.php b/src/Symfony/Component/HttpKernel/EventListener/DebugHandlersListener.php index 8a01569c9a..841c772c32 100644 --- a/src/Symfony/Component/HttpKernel/EventListener/DebugHandlersListener.php +++ b/src/Symfony/Component/HttpKernel/EventListener/DebugHandlersListener.php @@ -16,6 +16,7 @@ use Symfony\Component\Console\ConsoleEvents; use Symfony\Component\Console\Event\ConsoleEvent; use Symfony\Component\Console\Output\ConsoleOutputInterface; use Symfony\Component\ErrorHandler\ErrorHandler; +use Symfony\Component\ErrorHandler\Exception\FatalThrowableError; use Symfony\Component\EventDispatcher\Event; use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\HttpKernel\Debug\FileLinkFormatter; @@ -42,7 +43,7 @@ class DebugHandlersListener implements EventSubscriberInterface private $hasTerminatedWithException; /** - * @param callable|null $exceptionHandler A handler that will be called on Exception + * @param callable|null $exceptionHandler A handler that must support \Throwable instances that will be called on Exception * @param array|int $levels An array map of E_* to LogLevel::* or an integer bit field of E_* constants * @param int|null $throwAt Thrown errors in a bit field of E_* constants, or null to keep the current value * @param bool $scream Enables/disables screaming mode, where even silenced errors are logged @@ -106,10 +107,15 @@ class DebugHandlersListener implements EventSubscriberInterface if (method_exists($kernel = $event->getKernel(), 'terminateWithException')) { $request = $event->getRequest(); $hasRun = &$this->hasTerminatedWithException; - $this->exceptionHandler = static function (\Exception $e) use ($kernel, $request, &$hasRun) { + $this->exceptionHandler = static function (\Throwable $e) use ($kernel, $request, &$hasRun) { if ($hasRun) { throw $e; } + + if (!$e instanceof \Exception) { + $e = new FatalThrowableError($e); + } + $hasRun = true; $kernel->terminateWithException($e, $request); };