From 7057244890c205034a0499d518b0177ae53c338f Mon Sep 17 00:00:00 2001 From: Yonel Ceruto Date: Tue, 23 Apr 2019 19:03:00 -0400 Subject: [PATCH] Added ErrorHandler component --- .../FrameworkBundle/Console/Application.php | 2 +- .../FrameworkExtension.php | 1 + .../FrameworkBundle/FrameworkBundle.php | 4 +- .../Resources/config/debug_prod.xml | 3 +- .../Resources/config/error_renderer.xml | 39 + .../Bundle/FrameworkBundle/composer.json | 3 +- .../Controller/ExceptionController.php | 2 +- .../Controller/PreviewErrorController.php | 2 +- .../Compiler/ExceptionListenerPass.php | 2 +- .../Controller/ExceptionControllerTest.php | 2 +- .../Controller/PreviewErrorControllerTest.php | 2 +- src/Symfony/Bundle/TwigBundle/composer.json | 2 +- .../Controller/ExceptionController.php | 21 +- .../Resources/config/profiler.xml | 1 + .../WebProfilerExtensionTest.php | 2 + .../Bundle/WebProfilerBundle/composer.json | 2 +- src/Symfony/Component/Console/Application.php | 4 +- .../Component/Debug/BufferingLogger.php | 24 +- src/Symfony/Component/Debug/CHANGELOG.md | 12 + src/Symfony/Component/Debug/Debug.php | 4 + .../Component/Debug/DebugClassLoader.php | 2 +- src/Symfony/Component/Debug/ErrorHandler.php | 702 +---------------- .../Exception/ClassNotFoundException.php | 25 +- .../Debug/Exception/FatalErrorException.php | 66 +- .../Debug/Exception/FatalThrowableError.php | 40 +- .../Debug/Exception/FlattenException.php | 346 +-------- .../Debug/Exception/OutOfMemoryException.php | 10 +- .../Debug/Exception/SilencedErrorContext.php | 56 +- .../Exception/UndefinedFunctionException.php | 25 +- .../Exception/UndefinedMethodException.php | 25 +- .../Component/Debug/ExceptionHandler.php | 453 +---------- .../ClassNotFoundFatalErrorHandler.php | 180 +---- .../FatalErrorHandlerInterface.php | 19 +- .../UndefinedFunctionFatalErrorHandler.php | 71 +- .../UndefinedMethodFatalErrorHandler.php | 53 +- .../Debug/Tests/Fixtures/PEARClass.php | 5 - src/Symfony/Component/Debug/composer.json | 3 +- src/Symfony/Component/ErrorHandler/.gitignore | 3 + .../ErrorHandler/BufferingLogger.php | 37 + .../Component/ErrorHandler/CHANGELOG.md | 7 + .../DependencyInjection/ErrorHandlerPass.php | 66 ++ .../DependencyInjection/ErrorRenderer.php | 44 ++ .../Component/ErrorHandler/ErrorHandler.php | 716 ++++++++++++++++++ .../ErrorRenderer/ErrorRenderer.php | 71 ++ .../ErrorRenderer/ErrorRendererInterface.php | 36 + .../ErrorRenderer/HtmlErrorRenderer.php | 331 ++++++++ .../ErrorRenderer/JsonErrorRenderer.php | 52 ++ .../ErrorRenderer/TxtErrorRenderer.php | 98 +++ .../ErrorRenderer/XmlErrorRenderer.php | 117 +++ .../Exception/ClassNotFoundException.php | 36 + .../ErrorRendererNotFoundException.php | 16 + .../Exception/FatalErrorException.php | 77 ++ .../Exception/FatalThrowableError.php | 51 ++ .../Exception/FlattenException.php | 380 ++++++++++ .../Exception/OutOfMemoryException.php | 21 + .../Exception/SilencedErrorContext.php | 67 ++ .../Exception/UndefinedFunctionException.php | 36 + .../Exception/UndefinedMethodException.php | 36 + .../ErrorHandler/ExceptionHandler.php | 177 +++++ .../ClassNotFoundFatalErrorHandler.php | 193 +++++ .../FatalErrorHandlerInterface.php | 32 + .../UndefinedFunctionFatalErrorHandler.php | 84 ++ .../UndefinedMethodFatalErrorHandler.php | 66 ++ src/Symfony/Component/ErrorHandler/LICENSE | 19 + src/Symfony/Component/ErrorHandler/README.md | 12 + .../ErrorHandlerPassTest.php | 51 ++ .../DependencyInjection/ErrorRendererTest.php | 67 ++ .../Tests/ErrorHandlerTest.php | 68 +- .../Tests/ErrorRenderer/ErrorRendererTest.php | 62 ++ .../ErrorRenderer/HtmlErrorRendererTest.php | 27 + .../ErrorRenderer/JsonErrorRendererTest.php | 27 + .../ErrorRenderer/TxtErrorRendererTest.php | 27 + .../ErrorRenderer/XmlErrorRendererTest.php | 27 + .../Tests/Exception/FlattenExceptionTest.php | 6 +- .../Tests/ExceptionHandlerTest.php | 14 +- .../ClassNotFoundFatalErrorHandlerTest.php | 42 +- ...UndefinedFunctionFatalErrorHandlerTest.php | 12 +- .../UndefinedMethodFatalErrorHandlerTest.php | 8 +- .../ErrorHandlerThatUsesThePreviousOne.php | 2 +- .../Fixtures/LoggerThatSetAnErrorHandler.php | 4 +- .../ErrorHandler/Tests/Fixtures/PEARClass.php | 5 + .../Tests/Fixtures/ToStringThrower.php | 2 +- .../Tests/Fixtures/UndefinedFuncException.php | 7 + .../Tests/Fixtures2/RequiredTwice.php | 0 .../Tests/HeaderMock.php | 4 +- .../Tests/MockExceptionHandler.php | 4 +- .../Tests/phpt/decorate_exception_hander.phpt | 6 +- .../Tests/phpt/exception_rethrown.phpt | 2 +- .../phpt/fatal_with_nested_handlers.phpt | 8 +- .../Component/ErrorHandler/composer.json | 42 + .../Component/ErrorHandler/phpunit.xml.dist | 30 + .../DataCollector/ExceptionDataCollector.php | 2 +- .../DataCollector/LoggerDataCollector.php | 2 +- .../EventListener/DebugHandlersListener.php | 24 +- .../EventListener/ExceptionListener.php | 3 +- .../ExceptionDataCollectorTest.php | 2 +- .../DataCollector/LoggerDataCollectorTest.php | 4 +- .../DebugHandlersListenerTest.php | 4 +- .../Component/HttpKernel/composer.json | 3 +- ...ailedMessageToFailureTransportListener.php | 2 +- .../Messenger/Stamp/RedeliveryStamp.php | 2 +- .../Tests/Stamp/RedeliveryStampTest.php | 2 +- src/Symfony/Component/Messenger/composer.json | 5 +- .../VarDumper/Caster/ExceptionCaster.php | 2 +- .../VarDumper/Cloner/AbstractCloner.php | 2 +- 105 files changed, 3552 insertions(+), 2157 deletions(-) create mode 100644 src/Symfony/Bundle/FrameworkBundle/Resources/config/error_renderer.xml delete mode 100644 src/Symfony/Component/Debug/Tests/Fixtures/PEARClass.php create mode 100644 src/Symfony/Component/ErrorHandler/.gitignore create mode 100644 src/Symfony/Component/ErrorHandler/BufferingLogger.php create mode 100644 src/Symfony/Component/ErrorHandler/CHANGELOG.md create mode 100644 src/Symfony/Component/ErrorHandler/DependencyInjection/ErrorHandlerPass.php create mode 100644 src/Symfony/Component/ErrorHandler/DependencyInjection/ErrorRenderer.php create mode 100644 src/Symfony/Component/ErrorHandler/ErrorHandler.php create mode 100644 src/Symfony/Component/ErrorHandler/ErrorRenderer/ErrorRenderer.php create mode 100644 src/Symfony/Component/ErrorHandler/ErrorRenderer/ErrorRendererInterface.php create mode 100644 src/Symfony/Component/ErrorHandler/ErrorRenderer/HtmlErrorRenderer.php create mode 100644 src/Symfony/Component/ErrorHandler/ErrorRenderer/JsonErrorRenderer.php create mode 100644 src/Symfony/Component/ErrorHandler/ErrorRenderer/TxtErrorRenderer.php create mode 100644 src/Symfony/Component/ErrorHandler/ErrorRenderer/XmlErrorRenderer.php create mode 100644 src/Symfony/Component/ErrorHandler/Exception/ClassNotFoundException.php create mode 100644 src/Symfony/Component/ErrorHandler/Exception/ErrorRendererNotFoundException.php create mode 100644 src/Symfony/Component/ErrorHandler/Exception/FatalErrorException.php create mode 100644 src/Symfony/Component/ErrorHandler/Exception/FatalThrowableError.php create mode 100644 src/Symfony/Component/ErrorHandler/Exception/FlattenException.php create mode 100644 src/Symfony/Component/ErrorHandler/Exception/OutOfMemoryException.php create mode 100644 src/Symfony/Component/ErrorHandler/Exception/SilencedErrorContext.php create mode 100644 src/Symfony/Component/ErrorHandler/Exception/UndefinedFunctionException.php create mode 100644 src/Symfony/Component/ErrorHandler/Exception/UndefinedMethodException.php create mode 100644 src/Symfony/Component/ErrorHandler/ExceptionHandler.php create mode 100644 src/Symfony/Component/ErrorHandler/FatalErrorHandler/ClassNotFoundFatalErrorHandler.php create mode 100644 src/Symfony/Component/ErrorHandler/FatalErrorHandler/FatalErrorHandlerInterface.php create mode 100644 src/Symfony/Component/ErrorHandler/FatalErrorHandler/UndefinedFunctionFatalErrorHandler.php create mode 100644 src/Symfony/Component/ErrorHandler/FatalErrorHandler/UndefinedMethodFatalErrorHandler.php create mode 100644 src/Symfony/Component/ErrorHandler/LICENSE create mode 100644 src/Symfony/Component/ErrorHandler/README.md create mode 100644 src/Symfony/Component/ErrorHandler/Tests/DependencyInjection/ErrorHandlerPassTest.php create mode 100644 src/Symfony/Component/ErrorHandler/Tests/DependencyInjection/ErrorRendererTest.php rename src/Symfony/Component/{Debug => ErrorHandler}/Tests/ErrorHandlerTest.php (97%) create mode 100644 src/Symfony/Component/ErrorHandler/Tests/ErrorRenderer/ErrorRendererTest.php create mode 100644 src/Symfony/Component/ErrorHandler/Tests/ErrorRenderer/HtmlErrorRendererTest.php create mode 100644 src/Symfony/Component/ErrorHandler/Tests/ErrorRenderer/JsonErrorRendererTest.php create mode 100644 src/Symfony/Component/ErrorHandler/Tests/ErrorRenderer/TxtErrorRendererTest.php create mode 100644 src/Symfony/Component/ErrorHandler/Tests/ErrorRenderer/XmlErrorRendererTest.php rename src/Symfony/Component/{Debug => ErrorHandler}/Tests/Exception/FlattenExceptionTest.php (98%) rename src/Symfony/Component/{Debug => ErrorHandler}/Tests/ExceptionHandlerTest.php (86%) rename src/Symfony/Component/{Debug => ErrorHandler}/Tests/FatalErrorHandler/ClassNotFoundFatalErrorHandlerTest.php (80%) rename src/Symfony/Component/{Debug => ErrorHandler}/Tests/FatalErrorHandler/UndefinedFunctionFatalErrorHandlerTest.php (84%) rename src/Symfony/Component/{Debug => ErrorHandler}/Tests/FatalErrorHandler/UndefinedMethodFatalErrorHandlerTest.php (88%) rename src/Symfony/Component/{Debug => ErrorHandler}/Tests/Fixtures/ErrorHandlerThatUsesThePreviousOne.php (88%) rename src/Symfony/Component/{Debug => ErrorHandler}/Tests/Fixtures/LoggerThatSetAnErrorHandler.php (71%) create mode 100644 src/Symfony/Component/ErrorHandler/Tests/Fixtures/PEARClass.php rename src/Symfony/Component/{Debug => ErrorHandler}/Tests/Fixtures/ToStringThrower.php (89%) create mode 100644 src/Symfony/Component/ErrorHandler/Tests/Fixtures/UndefinedFuncException.php rename src/Symfony/Component/{Debug => ErrorHandler}/Tests/Fixtures2/RequiredTwice.php (100%) rename src/Symfony/Component/{Debug => ErrorHandler}/Tests/HeaderMock.php (86%) rename src/Symfony/Component/{Debug => ErrorHandler}/Tests/MockExceptionHandler.php (79%) rename src/Symfony/Component/{Debug => ErrorHandler}/Tests/phpt/decorate_exception_hander.phpt (78%) rename src/Symfony/Component/{Debug => ErrorHandler}/Tests/phpt/exception_rethrown.phpt (94%) rename src/Symfony/Component/{Debug => ErrorHandler}/Tests/phpt/fatal_with_nested_handlers.phpt (67%) create mode 100644 src/Symfony/Component/ErrorHandler/composer.json create mode 100644 src/Symfony/Component/ErrorHandler/phpunit.xml.dist diff --git a/src/Symfony/Bundle/FrameworkBundle/Console/Application.php b/src/Symfony/Bundle/FrameworkBundle/Console/Application.php index d14566c239..cd6d14759a 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\ErrorHandler\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/DependencyInjection/FrameworkExtension.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php index 19386d9721..0921af3a30 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php @@ -152,6 +152,7 @@ class FrameworkExtension extends Extension $loader->load('web.xml'); $loader->load('services.xml'); $loader->load('fragment_renderer.xml'); + $loader->load('error_renderer.xml'); $container->registerAliasForArgument('parameter_bag', PsrContainerInterface::class); diff --git a/src/Symfony/Bundle/FrameworkBundle/FrameworkBundle.php b/src/Symfony/Bundle/FrameworkBundle/FrameworkBundle.php index e5c714965c..33400a1a1c 100644 --- a/src/Symfony/Bundle/FrameworkBundle/FrameworkBundle.php +++ b/src/Symfony/Bundle/FrameworkBundle/FrameworkBundle.php @@ -29,10 +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\ErrorHandler\DependencyInjection\ErrorHandlerPass; +use Symfony\Component\ErrorHandler\ErrorHandler; use Symfony\Component\EventDispatcher\DependencyInjection\RegisterListenersPass; use Symfony\Component\Form\DependencyInjection\FormPass; use Symfony\Component\HttpFoundation\Request; @@ -90,6 +91,7 @@ class FrameworkBundle extends Bundle KernelEvents::FINISH_REQUEST, ]; + $this->addCompilerPassIfExists($container, ErrorHandlerPass::class); $container->addCompilerPass(new LoggerPass(), PassConfig::TYPE_BEFORE_OPTIMIZATION, -32); $container->addCompilerPass(new RegisterControllerArgumentLocatorsPass()); $container->addCompilerPass(new RemoveEmptyControllerArgumentLocatorsPass(), PassConfig::TYPE_BEFORE_REMOVING); diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/debug_prod.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/debug_prod.xml index e19d453d36..7220c73f17 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/debug_prod.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/debug_prod.xml @@ -19,9 +19,10 @@ null %debug.error_handler.throw_at% %kernel.debug% - + %kernel.debug% %kernel.charset% + diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/error_renderer.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/error_renderer.xml new file mode 100644 index 0000000000..a1273350bb --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/error_renderer.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + %kernel.debug% + %kernel.charset% + %debug.file_link_format% + + + + + %kernel.debug% + + + + + + %kernel.debug% + %kernel.charset% + + + + + %kernel.debug% + %kernel.charset% + + + diff --git a/src/Symfony/Bundle/FrameworkBundle/composer.json b/src/Symfony/Bundle/FrameworkBundle/composer.json index de007cd0b5..afa879edb5 100644 --- a/src/Symfony/Bundle/FrameworkBundle/composer.json +++ b/src/Symfony/Bundle/FrameworkBundle/composer.json @@ -22,7 +22,7 @@ "symfony/config": "^4.2|^5.0", "symfony/dependency-injection": "^4.4|^5.0", "symfony/http-foundation": "^4.3|^5.0", - "symfony/http-kernel": "^4.3|^5.0", + "symfony/http-kernel": "^4.4|^5.0", "symfony/polyfill-mbstring": "~1.0", "symfony/filesystem": "^3.4|^4.0|^5.0", "symfony/finder": "^3.4|^4.0|^5.0", @@ -69,6 +69,7 @@ "symfony/asset": "<3.4", "symfony/browser-kit": "<4.3", "symfony/console": "<4.3", + "symfony/debug": "<4.4", "symfony/dotenv": "<4.2", "symfony/dom-crawler": "<4.3", "symfony/form": "<4.3", diff --git a/src/Symfony/Bundle/TwigBundle/Controller/ExceptionController.php b/src/Symfony/Bundle/TwigBundle/Controller/ExceptionController.php index 5269a49de8..ba37e7fcac 100644 --- a/src/Symfony/Bundle/TwigBundle/Controller/ExceptionController.php +++ b/src/Symfony/Bundle/TwigBundle/Controller/ExceptionController.php @@ -11,7 +11,7 @@ namespace Symfony\Bundle\TwigBundle\Controller; -use Symfony\Component\Debug\Exception\FlattenException; +use Symfony\Component\ErrorHandler\Exception\FlattenException; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Log\DebugLoggerInterface; diff --git a/src/Symfony/Bundle/TwigBundle/Controller/PreviewErrorController.php b/src/Symfony/Bundle/TwigBundle/Controller/PreviewErrorController.php index c529cfbb46..0c37a7d4e7 100644 --- a/src/Symfony/Bundle/TwigBundle/Controller/PreviewErrorController.php +++ b/src/Symfony/Bundle/TwigBundle/Controller/PreviewErrorController.php @@ -11,7 +11,7 @@ namespace Symfony\Bundle\TwigBundle\Controller; -use Symfony\Component\Debug\Exception\FlattenException; +use Symfony\Component\ErrorHandler\Exception\FlattenException; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\HttpKernelInterface; diff --git a/src/Symfony/Bundle/TwigBundle/DependencyInjection/Compiler/ExceptionListenerPass.php b/src/Symfony/Bundle/TwigBundle/DependencyInjection/Compiler/ExceptionListenerPass.php index 6b6149ad57..2804aee393 100644 --- a/src/Symfony/Bundle/TwigBundle/DependencyInjection/Compiler/ExceptionListenerPass.php +++ b/src/Symfony/Bundle/TwigBundle/DependencyInjection/Compiler/ExceptionListenerPass.php @@ -28,7 +28,7 @@ class ExceptionListenerPass implements CompilerPassInterface } // register the exception controller only if Twig is enabled and required dependencies do exist - if (!class_exists('Symfony\Component\Debug\Exception\FlattenException') || !interface_exists('Symfony\Component\EventDispatcher\EventSubscriberInterface')) { + if (!class_exists('Symfony\Component\ErrorHandler\Exception\FlattenException') || !interface_exists('Symfony\Component\EventDispatcher\EventSubscriberInterface')) { $container->removeDefinition('twig.exception_listener'); } elseif ($container->hasParameter('templating.engines')) { $engines = $container->getParameter('templating.engines'); diff --git a/src/Symfony/Bundle/TwigBundle/Tests/Controller/ExceptionControllerTest.php b/src/Symfony/Bundle/TwigBundle/Tests/Controller/ExceptionControllerTest.php index 800da68c9b..a85c0b834c 100644 --- a/src/Symfony/Bundle/TwigBundle/Tests/Controller/ExceptionControllerTest.php +++ b/src/Symfony/Bundle/TwigBundle/Tests/Controller/ExceptionControllerTest.php @@ -13,7 +13,7 @@ namespace Symfony\Bundle\TwigBundle\Tests\Controller; use Symfony\Bundle\TwigBundle\Controller\ExceptionController; use Symfony\Bundle\TwigBundle\Tests\TestCase; -use Symfony\Component\Debug\Exception\FlattenException; +use Symfony\Component\ErrorHandler\Exception\FlattenException; use Symfony\Component\HttpFoundation\Request; use Twig\Environment; use Twig\Loader\ArrayLoader; diff --git a/src/Symfony/Bundle/TwigBundle/Tests/Controller/PreviewErrorControllerTest.php b/src/Symfony/Bundle/TwigBundle/Tests/Controller/PreviewErrorControllerTest.php index ae740e1ab2..51a8fd159b 100644 --- a/src/Symfony/Bundle/TwigBundle/Tests/Controller/PreviewErrorControllerTest.php +++ b/src/Symfony/Bundle/TwigBundle/Tests/Controller/PreviewErrorControllerTest.php @@ -13,7 +13,7 @@ namespace Symfony\Bundle\TwigBundle\Tests\Controller; use Symfony\Bundle\TwigBundle\Controller\PreviewErrorController; use Symfony\Bundle\TwigBundle\Tests\TestCase; -use Symfony\Component\Debug\Exception\FlattenException; +use Symfony\Component\ErrorHandler\Exception\FlattenException; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\HttpKernelInterface; diff --git a/src/Symfony/Bundle/TwigBundle/composer.json b/src/Symfony/Bundle/TwigBundle/composer.json index 750dc9f3c2..0f3cb58a0b 100644 --- a/src/Symfony/Bundle/TwigBundle/composer.json +++ b/src/Symfony/Bundle/TwigBundle/composer.json @@ -20,7 +20,7 @@ "symfony/config": "^4.2|^5.0", "symfony/twig-bridge": "^4.4|^5.0", "symfony/http-foundation": "^4.3|^5.0", - "symfony/http-kernel": "^4.1|^5.0", + "symfony/http-kernel": "^4.4|^5.0", "symfony/polyfill-ctype": "~1.8", "twig/twig": "~1.41|~2.10" }, diff --git a/src/Symfony/Bundle/WebProfilerBundle/Controller/ExceptionController.php b/src/Symfony/Bundle/WebProfilerBundle/Controller/ExceptionController.php index bc40f4f8dd..1e43adcf4c 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/Controller/ExceptionController.php +++ b/src/Symfony/Bundle/WebProfilerBundle/Controller/ExceptionController.php @@ -11,7 +11,7 @@ namespace Symfony\Bundle\WebProfilerBundle\Controller; -use Symfony\Component\Debug\ExceptionHandler; +use Symfony\Component\ErrorHandler\ErrorRenderer\HtmlErrorRenderer; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Debug\FileLinkFormatter; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; @@ -30,14 +30,18 @@ class ExceptionController protected $twig; protected $debug; protected $profiler; - private $fileLinkFormat; + private $errorRenderer; - public function __construct(Profiler $profiler = null, Environment $twig, bool $debug, FileLinkFormatter $fileLinkFormat = null) + public function __construct(Profiler $profiler = null, Environment $twig, bool $debug, FileLinkFormatter $fileLinkFormat = null, HtmlErrorRenderer $errorRenderer = null) { $this->profiler = $profiler; $this->twig = $twig; $this->debug = $debug; - $this->fileLinkFormat = $fileLinkFormat; + $this->errorRenderer = $errorRenderer; + + if (null === $errorRenderer) { + $this->errorRenderer = new HtmlErrorRenderer($debug, $this->twig->getCharset(), $fileLinkFormat); + } } /** @@ -61,9 +65,7 @@ class ExceptionController $template = $this->getTemplate(); if (!$this->twig->getLoader()->exists($template)) { - $handler = new ExceptionHandler($this->debug, $this->twig->getCharset(), $this->fileLinkFormat); - - return new Response($handler->getContent($exception), 200, ['Content-Type' => 'text/html']); + return new Response($this->errorRenderer->getBody($exception), 200, ['Content-Type' => 'text/html']); } $code = $exception->getStatusCode(); @@ -97,13 +99,10 @@ class ExceptionController $this->profiler->disable(); - $exception = $this->profiler->loadProfile($token)->getCollector('exception')->getException(); $template = $this->getTemplate(); if (!$this->templateExists($template)) { - $handler = new ExceptionHandler($this->debug, $this->twig->getCharset(), $this->fileLinkFormat); - - return new Response($handler->getStylesheet($exception), 200, ['Content-Type' => 'text/css']); + return new Response($this->errorRenderer->getStylesheet(), 200, ['Content-Type' => 'text/css']); } return new Response($this->twig->render('@WebProfiler/Collector/exception.css.twig'), 200, ['Content-Type' => 'text/css']); diff --git a/src/Symfony/Bundle/WebProfilerBundle/Resources/config/profiler.xml b/src/Symfony/Bundle/WebProfilerBundle/Resources/config/profiler.xml index 2f9fa66463..c1414bffb7 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/Resources/config/profiler.xml +++ b/src/Symfony/Bundle/WebProfilerBundle/Resources/config/profiler.xml @@ -27,6 +27,7 @@ %kernel.debug% + diff --git a/src/Symfony/Bundle/WebProfilerBundle/Tests/DependencyInjection/WebProfilerExtensionTest.php b/src/Symfony/Bundle/WebProfilerBundle/Tests/DependencyInjection/WebProfilerExtensionTest.php index b5633bb409..1658002468 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/Tests/DependencyInjection/WebProfilerExtensionTest.php +++ b/src/Symfony/Bundle/WebProfilerBundle/Tests/DependencyInjection/WebProfilerExtensionTest.php @@ -17,6 +17,7 @@ use Symfony\Component\DependencyInjection\Container; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\DependencyInjection\Reference; +use Symfony\Component\ErrorHandler\ErrorRenderer\HtmlErrorRenderer; use Symfony\Component\EventDispatcher\DependencyInjection\RegisterListenersPass; use Symfony\Component\EventDispatcher\EventDispatcher; @@ -53,6 +54,7 @@ class WebProfilerExtensionTest extends TestCase $this->kernel = $this->getMockBuilder('Symfony\\Component\\HttpKernel\\KernelInterface')->getMock(); $this->container = new ContainerBuilder(); + $this->container->register('error_handler.renderer.html', HtmlErrorRenderer::class); $this->container->register('event_dispatcher', EventDispatcher::class)->setPublic(true); $this->container->register('router', $this->getMockClass('Symfony\\Component\\Routing\\RouterInterface'))->setPublic(true); $this->container->register('twig', 'Twig\Environment')->setPublic(true); diff --git a/src/Symfony/Bundle/WebProfilerBundle/composer.json b/src/Symfony/Bundle/WebProfilerBundle/composer.json index 93176a5936..190fc6d117 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/composer.json +++ b/src/Symfony/Bundle/WebProfilerBundle/composer.json @@ -18,7 +18,7 @@ "require": { "php": "^7.1.3", "symfony/config": "^4.2|^5.0", - "symfony/http-kernel": "^4.3", + "symfony/http-kernel": "^4.4", "symfony/routing": "^3.4|^4.0|^5.0", "symfony/twig-bundle": "^4.2|^5.0", "symfony/var-dumper": "^3.4|^4.0|^5.0", diff --git a/src/Symfony/Component/Console/Application.php b/src/Symfony/Component/Console/Application.php index 563eaf33f8..52ac689609 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\Debug\ErrorHandler; -use Symfony\Component\Debug\Exception\FatalThrowableError; +use Symfony\Component\ErrorHandler\ErrorHandler; +use Symfony\Component\ErrorHandler\Exception\FatalThrowableError; use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Symfony\Component\EventDispatcher\LegacyEventDispatcherProxy; diff --git a/src/Symfony/Component/Debug/BufferingLogger.php b/src/Symfony/Component/Debug/BufferingLogger.php index e7db3a4ce4..c442c7b2b8 100644 --- a/src/Symfony/Component/Debug/BufferingLogger.php +++ b/src/Symfony/Component/Debug/BufferingLogger.php @@ -11,27 +11,13 @@ namespace Symfony\Component\Debug; -use Psr\Log\AbstractLogger; +use Symfony\Component\ErrorHandler\BufferingLogger as BaseBufferingLogger; + +@trigger_error(sprintf('The "%s" class is deprecated since Symfony 4.4, use "%s" instead.', BufferingLogger::class, BaseBufferingLogger::class), E_USER_DEPRECATED); /** - * A buffering logger that stacks logs for later. - * - * @author Nicolas Grekas + * @deprecated since Symfony 4.4, use Symfony\Component\ErrorHandler\BufferingLogger instead. */ -class BufferingLogger extends AbstractLogger +class BufferingLogger extends BaseBufferingLogger { - private $logs = []; - - public function log($level, $message, array $context = []) - { - $this->logs[] = [$level, $message, $context]; - } - - public function cleanLogs() - { - $logs = $this->logs; - $this->logs = []; - - return $logs; - } } diff --git a/src/Symfony/Component/Debug/CHANGELOG.md b/src/Symfony/Component/Debug/CHANGELOG.md index 367e834f01..a2841dc83a 100644 --- a/src/Symfony/Component/Debug/CHANGELOG.md +++ b/src/Symfony/Component/Debug/CHANGELOG.md @@ -1,6 +1,18 @@ CHANGELOG ========= +4.4.0 +----- + +* deprecated the `BufferingLogger`, `ErrorHandler` and `ExceptionHandler` classes, + they have been moved to the `ErrorHandler` component +* deprecated the `FatalErrorHandlerInterface`, `ClassNotFoundFatalErrorHandler`, + `UndefinedFunctionFatalErrorHandler` and `UndefinedMethodFatalErrorHandler` classes, + they have been moved to the `ErrorHandler` component +* deprecated the `ClassNotFoundException`, `FatalErrorException`, `FatalThrowableError`, + `FlattenException`, `OutOfMemoryException`, `SilencedErrorContext`, `UndefinedFunctionException`, + and `UndefinedMethodException`, they have been moved to the `ErrorHandler` component + 4.3.0 ----- diff --git a/src/Symfony/Component/Debug/Debug.php b/src/Symfony/Component/Debug/Debug.php index 5d2d55cf9f..20bb579301 100644 --- a/src/Symfony/Component/Debug/Debug.php +++ b/src/Symfony/Component/Debug/Debug.php @@ -11,6 +11,10 @@ namespace Symfony\Component\Debug; +use Symfony\Component\ErrorHandler\BufferingLogger; +use Symfony\Component\ErrorHandler\ErrorHandler; +use Symfony\Component\ErrorHandler\ExceptionHandler; + /** * Registers all the debug tools. * diff --git a/src/Symfony/Component/Debug/DebugClassLoader.php b/src/Symfony/Component/Debug/DebugClassLoader.php index ff9a8d72f9..c599931498 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\Debug\ErrorHandler'); + class_exists('Symfony\Component\ErrorHandler\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 a99a000b07..62c916694f 100644 --- a/src/Symfony/Component/Debug/ErrorHandler.php +++ b/src/Symfony/Component/Debug/ErrorHandler.php @@ -11,705 +11,13 @@ namespace Symfony\Component\Debug; -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; +use Symfony\Component\ErrorHandler\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); /** - * 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 + * @deprecated since Symfony 4.4, use Symfony\Component\ErrorHandler\ErrorHandler instead. */ -class ErrorHandler +class ErrorHandler extends BaseErrorHandler { - 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 fa98c4975d..ac0beab449 100644 --- a/src/Symfony/Component/Debug/Exception/ClassNotFoundException.php +++ b/src/Symfony/Component/Debug/Exception/ClassNotFoundException.php @@ -11,26 +11,13 @@ namespace Symfony\Component\Debug\Exception; +use Symfony\Component\ErrorHandler\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); + /** - * Class (or Trait or Interface) Not Found Exception. - * - * @author Konstanton Myakshin + * @deprecated since Symfony 4.4, use Symfony\Component\ErrorHandler\Exception\ClassNotFoundException instead. */ -class ClassNotFoundException extends FatalErrorException +class ClassNotFoundException extends BaseClassNotFoundException { - 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 93880fbc32..c203a101a0 100644 --- a/src/Symfony/Component/Debug/Exception/FatalErrorException.php +++ b/src/Symfony/Component/Debug/Exception/FatalErrorException.php @@ -11,67 +11,13 @@ namespace Symfony\Component\Debug\Exception; +use Symfony\Component\ErrorHandler\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); + /** - * Fatal Error Exception. - * - * @author Konstanton Myakshin + * @deprecated since Symfony 4.4, use Symfony\Component\ErrorHandler\Exception\FatalErrorException instead. */ -class FatalErrorException extends \ErrorException +class FatalErrorException extends BaseFatalErrorException { - 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 cdafb2a568..f87b6572c8 100644 --- a/src/Symfony/Component/Debug/Exception/FatalThrowableError.php +++ b/src/Symfony/Component/Debug/Exception/FatalThrowableError.php @@ -11,41 +11,13 @@ namespace Symfony\Component\Debug\Exception; +use Symfony\Component\ErrorHandler\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); + /** - * Fatal Throwable Error. - * - * @author Nicolas Grekas + * @deprecated since Symfony 4.4, use Symfony\Component\ErrorHandler\Exception\FatalThrowableError instead. */ -class FatalThrowableError extends FatalErrorException +class FatalThrowableError extends BaseFatalThrowableError { - 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 2e97ac39bb..aae6b261fe 100644 --- a/src/Symfony/Component/Debug/Exception/FlattenException.php +++ b/src/Symfony/Component/Debug/Exception/FlattenException.php @@ -11,349 +11,13 @@ namespace Symfony\Component\Debug\Exception; -use Symfony\Component\HttpFoundation\Exception\RequestExceptionInterface; -use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface; +use Symfony\Component\ErrorHandler\Exception\FlattenException as BaseFlattenException; + +@trigger_error(sprintf('The "%s" class is deprecated since Symfony 4.4, use "%s" instead.', FlattenException::class, BaseFlattenException::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\ErrorHandler\Exception\FlattenException instead. */ -class FlattenException +class FlattenException extends BaseFlattenException { - private $message; - private $code; - private $previous; - private $trace; - private $traceAsString; - private $class; - private $statusCode; - private $headers; - private $file; - private $line; - - public static function create(\Exception $exception, $statusCode = null, array $headers = []) - { - 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 fec1979836..43502edf9a 100644 --- a/src/Symfony/Component/Debug/Exception/OutOfMemoryException.php +++ b/src/Symfony/Component/Debug/Exception/OutOfMemoryException.php @@ -11,11 +11,13 @@ namespace Symfony\Component\Debug\Exception; +use Symfony\Component\ErrorHandler\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); + /** - * Out of memory exception. - * - * @author Nicolas Grekas + * @deprecated since Symfony 4.4, use Symfony\Component\ErrorHandler\Exception\OutOfMemoryException instead. */ -class OutOfMemoryException extends FatalErrorException +class OutOfMemoryException extends BaseOutOfMemoryException { } diff --git a/src/Symfony/Component/Debug/Exception/SilencedErrorContext.php b/src/Symfony/Component/Debug/Exception/SilencedErrorContext.php index 236c56ed0e..6e61709852 100644 --- a/src/Symfony/Component/Debug/Exception/SilencedErrorContext.php +++ b/src/Symfony/Component/Debug/Exception/SilencedErrorContext.php @@ -11,57 +11,13 @@ namespace Symfony\Component\Debug\Exception; +use Symfony\Component\ErrorHandler\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); + /** - * Data Object that represents a Silenced Error. - * - * @author Grégoire Pineau + * @deprecated since Symfony 4.4, use Symfony\Component\ErrorHandler\Exception\SilencedErrorContext instead. */ -class SilencedErrorContext implements \JsonSerializable +class SilencedErrorContext extends BaseSilencedErrorContext { - 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 d936c8759e..9bd8517986 100644 --- a/src/Symfony/Component/Debug/Exception/UndefinedFunctionException.php +++ b/src/Symfony/Component/Debug/Exception/UndefinedFunctionException.php @@ -11,26 +11,13 @@ namespace Symfony\Component\Debug\Exception; +use Symfony\Component\ErrorHandler\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); + /** - * Undefined Function Exception. - * - * @author Konstanton Myakshin + * @deprecated since Symfony 4.4, use Symfony\Component\ErrorHandler\Exception\UndefinedFunctionException instead. */ -class UndefinedFunctionException extends FatalErrorException +class UndefinedFunctionException extends BaseUndefinedFunctionException { - 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 f627561fe1..7382488f97 100644 --- a/src/Symfony/Component/Debug/Exception/UndefinedMethodException.php +++ b/src/Symfony/Component/Debug/Exception/UndefinedMethodException.php @@ -11,26 +11,13 @@ namespace Symfony\Component\Debug\Exception; +use Symfony\Component\ErrorHandler\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); + /** - * Undefined Method Exception. - * - * @author Grégoire Pineau + * @deprecated since Symfony 4.4, use Symfony\Component\ErrorHandler\Exception\UndefinedMethodException instead. */ -class UndefinedMethodException extends FatalErrorException +class UndefinedMethodException extends BaseUndefinedMethodException { - 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 743de65622..9d9097e55d 100644 --- a/src/Symfony/Component/Debug/ExceptionHandler.php +++ b/src/Symfony/Component/Debug/ExceptionHandler.php @@ -11,456 +11,13 @@ namespace Symfony\Component\Debug; -use Symfony\Component\Debug\Exception\FlattenException; -use Symfony\Component\Debug\Exception\OutOfMemoryException; -use Symfony\Component\HttpKernel\Debug\FileLinkFormatter; +use Symfony\Component\ErrorHandler\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); /** - * 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 + * @deprecated since Symfony 4.4, use Symfony\Component\ErrorHandler\ExceptionHandler instead. */ -class ExceptionHandler +class ExceptionHandler extends BaseExceptionHandler { - 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 \Exception|FlattenException $exception An \Exception or FlattenException instance - */ - public function sendPhpResponse($exception) - { - if (!$exception instanceof FlattenException) { - $exception = FlattenException::create($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 a0e2f770f0..476ea001b8 100644 --- a/src/Symfony/Component/Debug/FatalErrorHandler/ClassNotFoundFatalErrorHandler.php +++ b/src/Symfony/Component/Debug/FatalErrorHandler/ClassNotFoundFatalErrorHandler.php @@ -11,183 +11,13 @@ namespace Symfony\Component\Debug\FatalErrorHandler; -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; +use Symfony\Component\ErrorHandler\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); /** - * ErrorHandler for classes that do not exist. - * - * @author Fabien Potencier + * @deprecated since Symfony 4.4, use Symfony\Component\ErrorHandler\FatalErrorHandler\ClassNotFoundFatalErrorHandler instead. */ -class ClassNotFoundFatalErrorHandler implements FatalErrorHandlerInterface +class ClassNotFoundFatalErrorHandler extends BaseClassNotFoundFatalErrorHandler { - /** - * {@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 6b87eb30a1..d0a04de761 100644 --- a/src/Symfony/Component/Debug/FatalErrorHandler/FatalErrorHandlerInterface.php +++ b/src/Symfony/Component/Debug/FatalErrorHandler/FatalErrorHandlerInterface.php @@ -11,22 +11,13 @@ namespace Symfony\Component\Debug\FatalErrorHandler; -use Symfony\Component\Debug\Exception\FatalErrorException; +use Symfony\Component\ErrorHandler\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); /** - * Attempts to convert fatal errors to exceptions. - * - * @author Fabien Potencier + * @deprecated since Symfony 4.4, use Symfony\Component\ErrorHandler\FatalErrorHandler\FatalErrorHandlerInterface instead. */ -interface FatalErrorHandlerInterface +interface FatalErrorHandlerInterface extends BaseFatalErrorHandlerInterface { - /** - * 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 9eddeba5a6..ba50d4af9b 100644 --- a/src/Symfony/Component/Debug/FatalErrorHandler/UndefinedFunctionFatalErrorHandler.php +++ b/src/Symfony/Component/Debug/FatalErrorHandler/UndefinedFunctionFatalErrorHandler.php @@ -11,74 +11,13 @@ namespace Symfony\Component\Debug\FatalErrorHandler; -use Symfony\Component\Debug\Exception\FatalErrorException; -use Symfony\Component\Debug\Exception\UndefinedFunctionException; +use Symfony\Component\ErrorHandler\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); /** - * ErrorHandler for undefined functions. - * - * @author Fabien Potencier + * @deprecated since Symfony 4.4, use Symfony\Component\ErrorHandler\FatalErrorHandler\UndefinedFunctionFatalErrorHandler instead. */ -class UndefinedFunctionFatalErrorHandler implements FatalErrorHandlerInterface +class UndefinedFunctionFatalErrorHandler extends BaseUndefinedFunctionFatalErrorHandler { - /** - * {@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 1318cb13ba..149e07e608 100644 --- a/src/Symfony/Component/Debug/FatalErrorHandler/UndefinedMethodFatalErrorHandler.php +++ b/src/Symfony/Component/Debug/FatalErrorHandler/UndefinedMethodFatalErrorHandler.php @@ -11,56 +11,13 @@ namespace Symfony\Component\Debug\FatalErrorHandler; -use Symfony\Component\Debug\Exception\FatalErrorException; -use Symfony\Component\Debug\Exception\UndefinedMethodException; +use Symfony\Component\ErrorHandler\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); /** - * ErrorHandler for undefined methods. - * - * @author Grégoire Pineau + * @deprecated since Symfony 4.4, use Symfony\Component\ErrorHandler\FatalErrorHandler\UndefinedMethodFatalErrorHandler instead. */ -class UndefinedMethodFatalErrorHandler implements FatalErrorHandlerInterface +class UndefinedMethodFatalErrorHandler extends BaseUndefinedMethodFatalErrorHandler { - /** - * {@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/Debug/Tests/Fixtures/PEARClass.php b/src/Symfony/Component/Debug/Tests/Fixtures/PEARClass.php deleted file mode 100644 index 39f228182e..0000000000 --- a/src/Symfony/Component/Debug/Tests/Fixtures/PEARClass.php +++ /dev/null @@ -1,5 +0,0 @@ - + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\ErrorHandler; + +use Psr\Log\AbstractLogger; + +/** + * A buffering logger that stacks logs for later. + * + * @author Nicolas Grekas + */ +class BufferingLogger extends AbstractLogger +{ + private $logs = []; + + public function log($level, $message, array $context = []) + { + $this->logs[] = [$level, $message, $context]; + } + + public function cleanLogs() + { + $logs = $this->logs; + $this->logs = []; + + return $logs; + } +} diff --git a/src/Symfony/Component/ErrorHandler/CHANGELOG.md b/src/Symfony/Component/ErrorHandler/CHANGELOG.md new file mode 100644 index 0000000000..094072510d --- /dev/null +++ b/src/Symfony/Component/ErrorHandler/CHANGELOG.md @@ -0,0 +1,7 @@ +CHANGELOG +========= + +4.4.0 +----- + + * added the component diff --git a/src/Symfony/Component/ErrorHandler/DependencyInjection/ErrorHandlerPass.php b/src/Symfony/Component/ErrorHandler/DependencyInjection/ErrorHandlerPass.php new file mode 100644 index 0000000000..42beaf6e66 --- /dev/null +++ b/src/Symfony/Component/ErrorHandler/DependencyInjection/ErrorHandlerPass.php @@ -0,0 +1,66 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\ErrorHandler\DependencyInjection; + +use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; +use Symfony\Component\DependencyInjection\Compiler\ServiceLocatorTagPass; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Reference; +use Symfony\Component\ErrorHandler\ErrorRenderer\ErrorRendererInterface; + +/** + * @author Yonel Ceruto + */ +class ErrorHandlerPass implements CompilerPassInterface +{ + private $rendererService; + private $rendererTag; + + public function __construct(string $rendererService = 'error_handler.error_renderer', string $rendererTag = 'error_handler.renderer') + { + $this->rendererService = $rendererService; + $this->rendererTag = $rendererTag; + } + + /** + * {@inheritdoc} + */ + public function process(ContainerBuilder $container) + { + if (!$container->hasDefinition($this->rendererService)) { + return; + } + + $renderers = $registered = []; + foreach ($container->findTaggedServiceIds($this->rendererTag, true) as $serviceId => $tags) { + /** @var ErrorRendererInterface $class */ + $class = $container->getDefinition($serviceId)->getClass(); + + foreach ($tags as $tag) { + $format = $tag['format'] ?? $class::getFormat(); + if (!isset($registered[$format])) { + $priority = $tag['priority'] ?? 0; + $renderers[$priority][$format] = new Reference($serviceId); + $registered[$format] = true; + } + } + } + + if ($renderers) { + krsort($renderers); + $renderers = array_merge(...$renderers); + } + + $definition = $container->getDefinition($this->rendererService); + $definition->replaceArgument(0, ServiceLocatorTagPass::register($container, $renderers)); + } +} diff --git a/src/Symfony/Component/ErrorHandler/DependencyInjection/ErrorRenderer.php b/src/Symfony/Component/ErrorHandler/DependencyInjection/ErrorRenderer.php new file mode 100644 index 0000000000..dee08fd14c --- /dev/null +++ b/src/Symfony/Component/ErrorHandler/DependencyInjection/ErrorRenderer.php @@ -0,0 +1,44 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\ErrorHandler\DependencyInjection; + +use Psr\Container\ContainerInterface; +use Symfony\Component\ErrorHandler\ErrorRenderer\ErrorRenderer as BaseErrorRenderer; + +/** + * Lazily loads error renderers from the dependency injection container. + * + * @author Yonel Ceruto + */ +class ErrorRenderer extends BaseErrorRenderer +{ + private $container; + private $initialized = []; + + public function __construct(ContainerInterface $container) + { + $this->container = $container; + } + + /** + * {@inheritdoc} + */ + public function render($exception, string $format = 'html'): string + { + if (!isset($this->initialized[$format]) && $this->container->has($format)) { + $this->addRenderer($this->container->get($format), $format); + $this->initialized[$format] = true; + } + + return parent::render($exception, $format); + } +} diff --git a/src/Symfony/Component/ErrorHandler/ErrorHandler.php b/src/Symfony/Component/ErrorHandler/ErrorHandler.php new file mode 100644 index 0000000000..5793f34da7 --- /dev/null +++ b/src/Symfony/Component/ErrorHandler/ErrorHandler.php @@ -0,0 +1,716 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\ErrorHandler; + +use Psr\Log\LoggerInterface; +use Psr\Log\LogLevel; +use Symfony\Component\Debug\DebugClassLoader; +use Symfony\Component\ErrorHandler\Exception\FatalErrorException; +use Symfony\Component\ErrorHandler\Exception\FatalThrowableError; +use Symfony\Component\ErrorHandler\Exception\FlattenException; +use Symfony\Component\ErrorHandler\Exception\OutOfMemoryException; +use Symfony\Component\ErrorHandler\Exception\SilencedErrorContext; +use Symfony\Component\ErrorHandler\FatalErrorHandler\ClassNotFoundFatalErrorHandler; +use Symfony\Component\ErrorHandler\FatalErrorHandler\FatalErrorHandlerInterface; +use Symfony\Component\ErrorHandler\FatalErrorHandler\UndefinedFunctionFatalErrorHandler; +use Symfony\Component\ErrorHandler\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. + * + * @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 = $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/ErrorHandler/ErrorRenderer/ErrorRenderer.php b/src/Symfony/Component/ErrorHandler/ErrorRenderer/ErrorRenderer.php new file mode 100644 index 0000000000..e5c44127f8 --- /dev/null +++ b/src/Symfony/Component/ErrorHandler/ErrorRenderer/ErrorRenderer.php @@ -0,0 +1,71 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\ErrorHandler\ErrorRenderer; + +use Symfony\Component\ErrorHandler\Exception\ErrorRendererNotFoundException; +use Symfony\Component\ErrorHandler\Exception\FlattenException; + +/** + * Renders an Exception that represents a Response content. + * + * @see ErrorRendererInterface + * + * @author Yonel Ceruto + */ +class ErrorRenderer +{ + private $renderers = []; + + /** + * @param ErrorRendererInterface[] $renderers + */ + public function __construct(array $renderers) + { + foreach ($renderers as $renderer) { + if (!$renderer instanceof ErrorRendererInterface) { + throw new \InvalidArgumentException(sprintf('Error renderer "%s" must implement "%s".', \get_class($renderer), ErrorRendererInterface::class)); + } + + $this->addRenderer($renderer, $renderer::getFormat()); + } + } + + public function addRenderer(ErrorRendererInterface $renderer, string $format): self + { + $this->renderers[$format] = $renderer; + + return $this; + } + + /** + * Renders an Exception and returns the Response content. + * + * @param \Exception|FlattenException $exception An \Exception or FlattenException instance + * @param string $format The request format (html, json, xml, etc.) + * + * @return string The Response content as a string + * + * @throws ErrorRendererNotFoundException if no renderer is found + */ + public function render($exception, string $format = 'html'): string + { + if (!isset($this->renderers[$format])) { + throw new ErrorRendererNotFoundException(sprintf('No error renderer found for format "%s".', $format)); + } + + if (!$exception instanceof FlattenException) { + $exception = FlattenException::create($exception); + } + + return $this->renderers[$format]->render($exception); + } +} diff --git a/src/Symfony/Component/ErrorHandler/ErrorRenderer/ErrorRendererInterface.php b/src/Symfony/Component/ErrorHandler/ErrorRenderer/ErrorRendererInterface.php new file mode 100644 index 0000000000..0403b292c4 --- /dev/null +++ b/src/Symfony/Component/ErrorHandler/ErrorRenderer/ErrorRendererInterface.php @@ -0,0 +1,36 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\ErrorHandler\ErrorRenderer; + +use Symfony\Component\ErrorHandler\Exception\FlattenException; + +/** + * Interface implemented by all error renderers. + * + * @author Yonel Ceruto + */ +interface ErrorRendererInterface +{ + /** + * Gets the format of the content. + * + * @return string The content format + */ + public static function getFormat(): string; + + /** + * Renders an Exception and returns the Response content. + * + * @return string The Response content as a string + */ + public function render(FlattenException $exception): string; +} diff --git a/src/Symfony/Component/ErrorHandler/ErrorRenderer/HtmlErrorRenderer.php b/src/Symfony/Component/ErrorHandler/ErrorRenderer/HtmlErrorRenderer.php new file mode 100644 index 0000000000..545d1076fb --- /dev/null +++ b/src/Symfony/Component/ErrorHandler/ErrorRenderer/HtmlErrorRenderer.php @@ -0,0 +1,331 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\ErrorHandler\ErrorRenderer; + +use Symfony\Component\ErrorHandler\Exception\FlattenException; +use Symfony\Component\HttpKernel\Debug\FileLinkFormatter; + +/** + * @author Yonel Ceruto + */ +class HtmlErrorRenderer implements ErrorRendererInterface +{ + private const GHOST_ADDONS = [ + '02-14' => self::GHOST_HEART, + '02-29' => self::GHOST_PLUS, + '10-18' => self::GHOST_GIFT, + ]; + + private const GHOST_GIFT = 'M124.00534057617188,5.3606138080358505 C124.40059661865234,4.644828304648399 125.1237564086914,3.712414965033531 123.88127899169922,3.487462028861046 C123.53517150878906,3.3097832053899765 123.18894958496094,2.9953975528478622 122.8432846069336,3.345616325736046 C122.07421112060547,3.649444565176964 121.40750122070312,4.074306473135948 122.2164306640625,4.869479164481163 C122.57514953613281,5.3830065578222275 122.90142822265625,6.503447040915489 123.3077621459961,6.626829609274864 C123.55027770996094,6.210384353995323 123.7774658203125,5.785196766257286 124.00534057617188,5.3606138080358505 zM122.30630493164062,7.336987480521202 C121.60028076171875,6.076864704489708 121.03211975097656,4.72498320043087 120.16796875,3.562500938773155 C119.11695098876953,2.44033907353878 117.04605865478516,2.940566048026085 116.57544708251953,4.387995228171349 C115.95028686523438,5.819030746817589 117.2991714477539,7.527640804648399 118.826171875,7.348545059561729 C119.98493194580078,7.367936596274376 121.15027618408203,7.420116886496544 122.30630493164062,7.336987480521202 zM128.1732177734375,7.379541382193565 C129.67486572265625,7.17823551595211 130.53842163085938,5.287807449698448 129.68344116210938,4.032590612769127 C128.92578125,2.693056806921959 126.74605560302734,2.6463639587163925 125.98509216308594,4.007616028189659 C125.32617950439453,5.108129009604454 124.75428009033203,6.258124336600304 124.14962768554688,7.388818249106407 C125.48638916015625,7.465229496359825 126.8357162475586,7.447416767477989 128.1732177734375,7.379541382193565 zM130.6601104736328,8.991325363516808 C131.17202758789062,8.540884003043175 133.1543731689453,8.009847149252892 131.65304565429688,7.582054600119591 C131.2811279296875,7.476506695151329 130.84751892089844,6.99234913289547 130.5132598876953,7.124847874045372 C129.78744506835938,8.02728746831417 128.67140197753906,8.55669592320919 127.50616455078125,8.501235947012901 C127.27806091308594,8.576229080557823 126.11459350585938,8.38720129430294 126.428955078125,8.601900085806847 C127.25099182128906,9.070617660880089 128.0523223876953,9.579657539725304 128.902587890625,9.995706543326378 C129.49813842773438,9.678531631827354 130.0761260986328,9.329126343131065 130.6601104736328,8.991325363516808 zM118.96446990966797,9.246344551444054 C119.4022445678711,8.991325363516808 119.84001922607422,8.736305221915245 120.27779388427734,8.481284126639366 C118.93965911865234,8.414779648184776 117.40827941894531,8.607666000723839 116.39698791503906,7.531384453177452 C116.11186981201172,7.212117180228233 115.83845520019531,6.846597656607628 115.44329071044922,7.248530372977257 C114.96995544433594,7.574637398123741 113.5140609741211,7.908811077475548 114.63501739501953,8.306883797049522 C115.61112976074219,8.883499130606651 116.58037567138672,9.474181160330772 117.58061218261719,10.008124336600304 C118.05723571777344,9.784612640738487 118.50651550292969,9.5052699893713 118.96446990966797,9.246344551444054 zM125.38018035888672,12.091858848929405 C125.9474868774414,11.636047348380089 127.32159423828125,11.201767906546593 127.36749267578125,10.712632164359093 C126.08487701416016,9.974547371268272 124.83960723876953,9.152772888541222 123.49772644042969,8.528907760977745 C123.03594207763672,8.353693947196007 122.66152954101562,8.623294815421104 122.28982543945312,8.857431396842003 C121.19065856933594,9.51122473180294 120.06505584716797,10.12446115911007 119.00167083740234,10.835315689444542 C120.39238739013672,11.69529627263546 121.79983520507812,12.529837593436241 123.22095489501953,13.338589653372765 C123.94580841064453,12.932025894522667 124.66128540039062,12.508862480521202 125.38018035888672,12.091858848929405 zM131.07164001464844,13.514615997672081 C131.66018676757812,13.143282875418663 132.2487335205078,12.771927818655968 132.8372802734375,12.400571808218956 C132.8324737548828,11.156818374991417 132.8523406982422,9.912529930472374 132.81829833984375,8.669195160269737 C131.63046264648438,9.332009300589561 130.45948791503906,10.027913078665733 129.30828857421875,10.752535805106163 C129.182373046875,12.035354599356651 129.24623107910156,13.33940313756466 129.27359008789062,14.628684982657433 C129.88104248046875,14.27079389989376 130.4737548828125,13.888019546866417 131.07164001464844,13.514640793204308 zM117.26847839355469,12.731024727225304 C117.32825469970703,11.67083452641964 117.45709991455078,10.46224020421505 116.17853546142578,10.148179039359093 C115.37110900878906,9.77159021794796 114.25194549560547,8.806716904044151 113.62991333007812,8.81639002263546 C113.61052703857422,10.0110072940588 113.62078857421875,11.20585821568966 113.61869049072266,12.400571808218956 C114.81139373779297,13.144886955618858 115.98292541503906,13.925040230154991 117.20137023925781,14.626662239432335 C117.31951141357422,14.010867103934288 117.24227905273438,13.35805033147335 117.26847839355469,12.731024727225304 zM125.80937957763672,16.836034759879112 C126.51483917236328,16.390663132071495 127.22030639648438,15.945291504263878 127.92576599121094,15.49991987645626 C127.92250061035156,14.215868934988976 127.97560119628906,12.929980263113976 127.91757202148438,11.647302612662315 C127.14225769042969,11.869626984000206 126.25550079345703,12.556857094168663 125.43866729736328,12.983742699027061 C124.82704162597656,13.342005714774132 124.21542358398438,13.700271591544151 123.60379028320312,14.05853746831417 C123.61585235595703,15.429577812552452 123.57081604003906,16.803131088614464 123.64839172363281,18.172149643301964 C124.37957000732422,17.744937881827354 125.09130859375,17.284801468253136 125.80937957763672,16.836034759879112 zM122.8521499633789,16.115344032645226 C122.8521499633789,15.429741844534874 122.8521499633789,14.744139656424522 122.8521499633789,14.05853746831417 C121.43595123291016,13.230924591422081 120.02428436279297,12.395455345511436 118.60256958007812,11.577354416251183 C118.52394104003906,12.888403877615929 118.56887817382812,14.204405769705772 118.55702209472656,15.517732605338097 C119.97289276123047,16.4041957706213 121.37410736083984,17.314891800284386 122.80789947509766,18.172149643301964 C122.86368560791016,17.488990768790245 122.84332275390625,16.800363525748253 122.8521499633789,16.115344032645226 zM131.10684204101562,18.871450409293175 C131.68399047851562,18.48711584508419 132.2611541748047,18.10278509557247 132.8383026123047,17.718475326895714 C132.81423950195312,16.499977096915245 132.89776611328125,15.264989838004112 132.77627563476562,14.05993078649044 C131.5760040283203,14.744719490408897 130.41763305664062,15.524359688162804 129.23875427246094,16.255397781729698 C129.26707458496094,17.516149505972862 129.18060302734375,18.791316971182823 129.3108367919922,20.041303619742393 C129.91973876953125,19.667551025748253 130.51010131835938,19.264152511954308 131.10684204101562,18.871450409293175 zM117.2557373046875,18.188333496451378 C117.25104522705078,17.549470886588097 117.24633026123047,16.91058538854122 117.24163055419922,16.271720871329308 C116.04924774169922,15.525708183646202 114.87187957763672,14.75476549565792 113.66158294677734,14.038097366690636 C113.5858383178711,15.262084946036339 113.62901306152344,16.49083898961544 113.61761474609375,17.717010483145714 C114.82051086425781,18.513254150748253 116.00987243652344,19.330610260367393 117.22888946533203,20.101993545889854 C117.27559661865234,19.466014847159386 117.25241088867188,18.825733169913292 117.2557373046875,18.188333496451378 zM125.8398666381836,22.38675306737423 C126.54049682617188,21.921453461050987 127.24110412597656,21.456151947379112 127.94172668457031,20.99083136022091 C127.94009399414062,19.693386062979698 127.96646118164062,18.395381912589073 127.93160247802734,17.098379120230675 C126.50540924072266,17.97775076329708 125.08877563476562,18.873308166861534 123.68258666992188,19.78428266942501 C123.52366638183594,21.03710363805294 123.626708984375,22.32878302037716 123.62647247314453,23.595300659537315 C124.06291198730469,23.86113165318966 125.1788101196289,22.68297766149044 125.8398666381836,22.38675306737423 zM122.8521499633789,21.83134649693966 C122.76741790771484,20.936696991324425 123.21651458740234,19.67745779454708 122.0794677734375,19.330633148550987 C120.93280029296875,18.604360565543175 119.7907485961914,17.870157226920128 118.62899780273438,17.16818617284298 C118.45966339111328,18.396427139639854 118.63676452636719,19.675991043448448 118.50668334960938,20.919256195425987 C119.89984130859375,21.92635916173458 121.32942199707031,22.88914106786251 122.78502655029297,23.803510650992393 C122.90177917480469,23.1627406924963 122.82917022705078,22.48402212560177 122.8521499633789,21.83134649693966 zM117.9798355102539,21.59483526647091 C116.28416442871094,20.46288488805294 114.58848571777344,19.330957397818565 112.892822265625,18.199007019400597 C112.89473724365234,14.705654129385948 112.84647369384766,11.211485847830772 112.90847778320312,7.718807205557823 C113.7575912475586,7.194885239005089 114.66117858886719,6.765397056937218 115.5350341796875,6.284702762961388 C114.97061157226562,4.668964847922325 115.78496551513672,2.7054970115423203 117.42159271240234,2.1007001250982285 C118.79354095458984,1.537783369421959 120.44731903076172,2.0457767099142075 121.32200622558594,3.23083733022213 C121.95732116699219,2.9050118774175644 122.59264373779297,2.5791852325201035 123.22796630859375,2.253336176276207 C123.86669921875,2.5821153968572617 124.50543975830078,2.9108948558568954 125.1441650390625,3.23967407643795 C126.05941009521484,2.154020771384239 127.62747192382812,1.5344576686620712 128.986328125,2.1429056972265244 C130.61741638183594,2.716217741370201 131.50650024414062,4.675290569663048 130.9215545654297,6.2884936183691025 C131.8018341064453,6.78548763692379 132.7589111328125,7.1738648265600204 133.5660400390625,7.780336365103722 C133.60182189941406,11.252970680594444 133.56637573242188,14.726140961050987 133.5631103515625,18.199007019400597 C130.18914794921875,20.431867584586143 126.86984252929688,22.74994657933712 123.44108581542969,24.897907242178917 C122.44406127929688,24.897628769278526 121.5834732055664,23.815067276358604 120.65831756591797,23.37616156041622 C119.76387023925781,22.784828171133995 118.87168884277344,22.19007681310177 117.9798355102539,21.59483526647091 z'; + private const GHOST_HEART = 'M125.91386369681868,8.305165958366445 C128.95033202169043,-0.40540639102854037 140.8469835342744,8.305165958366445 125.91386369681868,19.504526138305664 C110.98208663272044,8.305165958366445 122.87795231771452,-0.40540639102854037 125.91386369681868,8.305165958366445 z'; + private const GHOST_PLUS = 'M111.36824226379395,8.969108581542969 L118.69175148010254,8.969108581542969 L118.69175148010254,1.6455793380737305 L126.20429420471191,1.6455793380737305 L126.20429420471191,8.969108581542969 L133.52781105041504,8.969108581542969 L133.52781105041504,16.481630325317383 L126.20429420471191,16.481630325317383 L126.20429420471191,23.805158615112305 L118.69175148010254,23.805158615112305 L118.69175148010254,16.481630325317383 L111.36824226379395,16.481630325317383 z'; + + private $debug; + private $charset; + 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; + } + + /** + * {@inheritdoc} + */ + public static function getFormat(): string + { + return 'html'; + } + + /** + * {@inheritdoc} + */ + public function render(FlattenException $exception): string + { + $css = $this->getStylesheet(); + $body = $this->getBody($exception); + + return << + + + + + {$exception->getTitle()} + + + + $body + + +EOF; + } + + /** + * 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; + } + + /** + * Gets the HTML content associated with the given exception. + * + * @return string The content as a string + */ + public function getBody(FlattenException $exception) + { + if (!$this->debug) { + return << +

Oops! An Error Occurred

+

The server returned a "{$exception->getStatusCode()} {$exception->getTitle()}".

+

+ Something is broken. Please let us know what you were doing when this error occurred. + We will fix it as soon as possible. Sorry for any inconvenience caused. +

+ +EOF; + } + + if (404 === $exception->getStatusCode()) { + $exceptionMessage = 'Sorry, the page you are looking for could not be found.'; + } else { + $exceptionMessage = $this->escapeHtml($exception->getMessage()); + } + + $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'], $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); + $exceptionMessage = sprintf('Exception thrown when handling an exception (%s: %s)', $e->getClass(), $this->escapeHtml($e->getMessage())); + } else { + $exceptionMessage = 'Whoops, looks like something went wrong.'; + } + } + + $symfonyGhostImageContents = $this->getSymfonyGhostAsSvg(); + + return << +
+
+

$exceptionMessage

+
$symfonyGhostImageContents
+
+
+ + +
+ $content +
+EOF; + } + + /** + * Gets the stylesheet associated with the given exception. + * + * @return string The stylesheet as a string + */ + public function getStylesheet(): string + { + 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; } + h2 { font-size: 18px; } +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 formatClass($class): string + { + $parts = explode('\\', $class); + + return sprintf('%s', $class, array_pop($parts)); + } + + private function formatPath(string $path, int $line): string + { + $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. + */ + private function formatArgs(array $args): string + { + $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(string $str): string + { + return htmlspecialchars($str, ENT_COMPAT | ENT_SUBSTITUTE, $this->charset); + } + + private function getSymfonyGhostAsSvg(): string + { + return ''.$this->addElementToGhost().''; + } + + private function addElementToGhost(): string + { + if (!isset(self::GHOST_ADDONS[date('m-d')])) { + return ''; + } + + return ''; + } +} diff --git a/src/Symfony/Component/ErrorHandler/ErrorRenderer/JsonErrorRenderer.php b/src/Symfony/Component/ErrorHandler/ErrorRenderer/JsonErrorRenderer.php new file mode 100644 index 0000000000..0c122be65f --- /dev/null +++ b/src/Symfony/Component/ErrorHandler/ErrorRenderer/JsonErrorRenderer.php @@ -0,0 +1,52 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\ErrorHandler\ErrorRenderer; + +use Symfony\Component\ErrorHandler\Exception\FlattenException; + +/** + * @author Yonel Ceruto + */ +class JsonErrorRenderer implements ErrorRendererInterface +{ + private $debug; + + public function __construct(bool $debug = true) + { + $this->debug = $debug; + } + + /** + * {@inheritdoc} + */ + public static function getFormat(): string + { + return 'json'; + } + + /** + * {@inheritdoc} + */ + public function render(FlattenException $exception): string + { + $content = [ + 'title' => $exception->getTitle(), + 'status' => $exception->getStatusCode(), + 'detail' => $exception->getMessage(), + ]; + if ($this->debug) { + $content['exceptions'] = $exception->toArray(); + } + + return (string) json_encode($content); + } +} diff --git a/src/Symfony/Component/ErrorHandler/ErrorRenderer/TxtErrorRenderer.php b/src/Symfony/Component/ErrorHandler/ErrorRenderer/TxtErrorRenderer.php new file mode 100644 index 0000000000..629fbe9e73 --- /dev/null +++ b/src/Symfony/Component/ErrorHandler/ErrorRenderer/TxtErrorRenderer.php @@ -0,0 +1,98 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\ErrorHandler\ErrorRenderer; + +use Symfony\Component\ErrorHandler\Exception\FlattenException; + +/** + * @author Yonel Ceruto + */ +class TxtErrorRenderer implements ErrorRendererInterface +{ + private $debug; + private $charset; + + public function __construct(bool $debug = true, string $charset = null) + { + $this->debug = $debug; + $this->charset = $charset ?: (ini_get('default_charset') ?: 'UTF-8'); + } + + /** + * {@inheritdoc} + */ + public static function getFormat(): string + { + return 'txt'; + } + + /** + * {@inheritdoc} + */ + public function render(FlattenException $exception): string + { + $content = sprintf("[title] %s\n", $exception->getTitle()); + $content .= sprintf("[status] %s\n", $exception->getStatusCode()); + $content .= sprintf("[detail] %s\n", $exception->getMessage()); + + if ($this->debug) { + foreach ($exception->toArray() as $i => $e) { + $content .= sprintf("[%d] %s: %s\n", $i + 1, $e['class'], $e['message']); + foreach ($e['trace'] as $trace) { + if ($trace['function']) { + $content .= sprintf('at %s%s%s(%s) ', $trace['class'], $trace['type'], $trace['function'], $this->formatArgs($trace['args'])); + } + if (isset($trace['file'], $trace['line'])) { + $content .= $this->formatPath($trace['file'], $trace['line']); + } + $content .= "\n"; + } + } + } + + return $content; + } + + private function formatPath(string $path, int $line): string + { + $file = preg_match('#[^/\\\\]*+$#', $path, $file) ? $file[0] : $path; + + return sprintf('in %s %s', $path, 0 < $line ? ' line '.$line : ''); + } + + /** + * Formats an array as a string. + */ + private function formatArgs(array $args): string + { + $result = []; + foreach ($args as $key => $item) { + if ('object' === $item[0]) { + $formattedValue = sprintf('object(%s)', $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", '', var_export($item[1], true)); + } + + $result[] = \is_int($key) ? $formattedValue : sprintf("'%s' => %s", $key, $formattedValue); + } + + return implode(', ', $result); + } +} diff --git a/src/Symfony/Component/ErrorHandler/ErrorRenderer/XmlErrorRenderer.php b/src/Symfony/Component/ErrorHandler/ErrorRenderer/XmlErrorRenderer.php new file mode 100644 index 0000000000..d9d505aabf --- /dev/null +++ b/src/Symfony/Component/ErrorHandler/ErrorRenderer/XmlErrorRenderer.php @@ -0,0 +1,117 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\ErrorHandler\ErrorRenderer; + +use Symfony\Component\ErrorHandler\Exception\FlattenException; + +/** + * @author Yonel Ceruto + */ +class XmlErrorRenderer implements ErrorRendererInterface +{ + private $debug; + private $charset; + + public function __construct(bool $debug = true, string $charset = null) + { + $this->debug = $debug; + $this->charset = $charset ?: (ini_get('default_charset') ?: 'UTF-8'); + } + + /** + * {@inheritdoc} + */ + public static function getFormat(): string + { + return 'xml'; + } + + /** + * {@inheritdoc} + */ + public function render(FlattenException $exception): string + { + $message = $this->escapeXml($exception->getMessage()); + + $exceptions = ''; + if ($this->debug) { + $exceptions .= ''; + foreach ($exception->toArray() as $e) { + $exceptions .= sprintf('', $e['class'], $this->escapeXml($e['message'])); + foreach ($e['trace'] as $trace) { + $exceptions .= ''; + if ($trace['function']) { + $exceptions .= sprintf('at %s%s%s(%s) ', $trace['class'], $trace['type'], $trace['function'], $this->formatArgs($trace['args'])); + } + if (isset($trace['file'], $trace['line'])) { + $exceptions .= $this->formatPath($trace['file'], $trace['line']); + } + $exceptions .= ''; + } + $exceptions .= ''; + } + $exceptions .= ''; + } + + return << + + {$exception->getTitle()} + {$exception->getStatusCode()} + {$message} + {$exceptions} + +EOF; + } + + /** + * XML-encodes a string. + */ + private function escapeXml(string $str): string + { + return htmlspecialchars($str, ENT_COMPAT | ENT_SUBSTITUTE, $this->charset); + } + + private function formatPath(string $path, int $line): string + { + $file = $this->escapeXml(preg_match('#[^/\\\\]*+$#', $path, $file) ? $file[0] : $path); + + return sprintf('in %s %s', $this->escapeXml($path), 0 < $line ? ' line '.$line : ''); + } + + /** + * Formats an array as a string. + */ + private function formatArgs(array $args): string + { + $result = []; + foreach ($args as $key => $item) { + if ('object' === $item[0]) { + $formattedValue = sprintf('object(%s)', $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->escapeXml(var_export($item[1], true))); + } + + $result[] = \is_int($key) ? $formattedValue : sprintf("'%s' => %s", $this->escapeXml($key), $formattedValue); + } + + return implode(', ', $result); + } +} diff --git a/src/Symfony/Component/ErrorHandler/Exception/ClassNotFoundException.php b/src/Symfony/Component/ErrorHandler/Exception/ClassNotFoundException.php new file mode 100644 index 0000000000..b0638826d6 --- /dev/null +++ b/src/Symfony/Component/ErrorHandler/Exception/ClassNotFoundException.php @@ -0,0 +1,36 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\ErrorHandler\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/ErrorHandler/Exception/ErrorRendererNotFoundException.php b/src/Symfony/Component/ErrorHandler/Exception/ErrorRendererNotFoundException.php new file mode 100644 index 0000000000..45db78a83e --- /dev/null +++ b/src/Symfony/Component/ErrorHandler/Exception/ErrorRendererNotFoundException.php @@ -0,0 +1,16 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\ErrorHandler\Exception; + +class ErrorRendererNotFoundException extends \RuntimeException +{ +} diff --git a/src/Symfony/Component/ErrorHandler/Exception/FatalErrorException.php b/src/Symfony/Component/ErrorHandler/Exception/FatalErrorException.php new file mode 100644 index 0000000000..4269356d57 --- /dev/null +++ b/src/Symfony/Component/ErrorHandler/Exception/FatalErrorException.php @@ -0,0 +1,77 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\ErrorHandler\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/ErrorHandler/Exception/FatalThrowableError.php b/src/Symfony/Component/ErrorHandler/Exception/FatalThrowableError.php new file mode 100644 index 0000000000..a690c83597 --- /dev/null +++ b/src/Symfony/Component/ErrorHandler/Exception/FatalThrowableError.php @@ -0,0 +1,51 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\ErrorHandler\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/ErrorHandler/Exception/FlattenException.php b/src/Symfony/Component/ErrorHandler/Exception/FlattenException.php new file mode 100644 index 0000000000..cb63888cd7 --- /dev/null +++ b/src/Symfony/Component/ErrorHandler/Exception/FlattenException.php @@ -0,0 +1,380 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\ErrorHandler\Exception; + +use Symfony\Component\HttpFoundation\Exception\RequestExceptionInterface; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface; + +/** + * 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 + */ +class FlattenException +{ + private $title; + private $message; + private $code; + private $previous; + private $trace; + private $traceAsString; + private $class; + private $statusCode; + private $headers; + private $file; + private $line; + + public static function create(\Exception $exception, $statusCode = null, array $headers = []) + { + 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; + } + + if (class_exists(Response::class) && isset(Response::$statusTexts[$statusCode])) { + $title = Response::$statusTexts[$statusCode]; + } else { + $title = 'Whoops, looks like something went wrong.'; + } + + $e->setTitle($title); + $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 getTitle() + { + return $this->title; + } + + public function setTitle(string $title): self + { + $this->title = $title; + + 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/ErrorHandler/Exception/OutOfMemoryException.php b/src/Symfony/Component/ErrorHandler/Exception/OutOfMemoryException.php new file mode 100644 index 0000000000..18c367596f --- /dev/null +++ b/src/Symfony/Component/ErrorHandler/Exception/OutOfMemoryException.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\ErrorHandler\Exception; + +/** + * Out of memory exception. + * + * @author Nicolas Grekas + */ +class OutOfMemoryException extends FatalErrorException +{ +} diff --git a/src/Symfony/Component/ErrorHandler/Exception/SilencedErrorContext.php b/src/Symfony/Component/ErrorHandler/Exception/SilencedErrorContext.php new file mode 100644 index 0000000000..2c4ae69db4 --- /dev/null +++ b/src/Symfony/Component/ErrorHandler/Exception/SilencedErrorContext.php @@ -0,0 +1,67 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\ErrorHandler\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/ErrorHandler/Exception/UndefinedFunctionException.php b/src/Symfony/Component/ErrorHandler/Exception/UndefinedFunctionException.php new file mode 100644 index 0000000000..bb2f46564d --- /dev/null +++ b/src/Symfony/Component/ErrorHandler/Exception/UndefinedFunctionException.php @@ -0,0 +1,36 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\ErrorHandler\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/ErrorHandler/Exception/UndefinedMethodException.php b/src/Symfony/Component/ErrorHandler/Exception/UndefinedMethodException.php new file mode 100644 index 0000000000..12efdc716c --- /dev/null +++ b/src/Symfony/Component/ErrorHandler/Exception/UndefinedMethodException.php @@ -0,0 +1,36 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\ErrorHandler\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/ErrorHandler/ExceptionHandler.php b/src/Symfony/Component/ErrorHandler/ExceptionHandler.php new file mode 100644 index 0000000000..577ff49230 --- /dev/null +++ b/src/Symfony/Component/ErrorHandler/ExceptionHandler.php @@ -0,0 +1,177 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\ErrorHandler; + +use Symfony\Component\ErrorHandler\ErrorRenderer\HtmlErrorRenderer; +use Symfony\Component\ErrorHandler\Exception\FlattenException; +use Symfony\Component\ErrorHandler\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 \Exception|FlattenException $exception An \Exception or FlattenException instance + */ + public function sendPhpResponse($exception) + { + if (!$exception instanceof FlattenException) { + $exception = FlattenException::create($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/ErrorHandler/FatalErrorHandler/ClassNotFoundFatalErrorHandler.php b/src/Symfony/Component/ErrorHandler/FatalErrorHandler/ClassNotFoundFatalErrorHandler.php new file mode 100644 index 0000000000..c84b3e8e4c --- /dev/null +++ b/src/Symfony/Component/ErrorHandler/FatalErrorHandler/ClassNotFoundFatalErrorHandler.php @@ -0,0 +1,193 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\ErrorHandler\FatalErrorHandler; + +use Composer\Autoload\ClassLoader as ComposerClassLoader; +use Symfony\Component\ClassLoader\ClassLoader as SymfonyClassLoader; +use Symfony\Component\Debug\DebugClassLoader; +use Symfony\Component\ErrorHandler\Exception\ClassNotFoundException; +use Symfony\Component\ErrorHandler\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/ErrorHandler/FatalErrorHandler/FatalErrorHandlerInterface.php b/src/Symfony/Component/ErrorHandler/FatalErrorHandler/FatalErrorHandlerInterface.php new file mode 100644 index 0000000000..afa8b7d236 --- /dev/null +++ b/src/Symfony/Component/ErrorHandler/FatalErrorHandler/FatalErrorHandlerInterface.php @@ -0,0 +1,32 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\ErrorHandler\FatalErrorHandler; + +use Symfony\Component\ErrorHandler\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/ErrorHandler/FatalErrorHandler/UndefinedFunctionFatalErrorHandler.php b/src/Symfony/Component/ErrorHandler/FatalErrorHandler/UndefinedFunctionFatalErrorHandler.php new file mode 100644 index 0000000000..9e3affb14d --- /dev/null +++ b/src/Symfony/Component/ErrorHandler/FatalErrorHandler/UndefinedFunctionFatalErrorHandler.php @@ -0,0 +1,84 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\ErrorHandler\FatalErrorHandler; + +use Symfony\Component\ErrorHandler\Exception\FatalErrorException; +use Symfony\Component\ErrorHandler\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/ErrorHandler/FatalErrorHandler/UndefinedMethodFatalErrorHandler.php b/src/Symfony/Component/ErrorHandler/FatalErrorHandler/UndefinedMethodFatalErrorHandler.php new file mode 100644 index 0000000000..49de274469 --- /dev/null +++ b/src/Symfony/Component/ErrorHandler/FatalErrorHandler/UndefinedMethodFatalErrorHandler.php @@ -0,0 +1,66 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\ErrorHandler\FatalErrorHandler; + +use Symfony\Component\ErrorHandler\Exception\FatalErrorException; +use Symfony\Component\ErrorHandler\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/ErrorHandler/LICENSE b/src/Symfony/Component/ErrorHandler/LICENSE new file mode 100644 index 0000000000..1a1869751d --- /dev/null +++ b/src/Symfony/Component/ErrorHandler/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2019 Fabien Potencier + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/src/Symfony/Component/ErrorHandler/README.md b/src/Symfony/Component/ErrorHandler/README.md new file mode 100644 index 0000000000..7b1aa81967 --- /dev/null +++ b/src/Symfony/Component/ErrorHandler/README.md @@ -0,0 +1,12 @@ +ErrorHandler Component +====================== + +The ErrorHandler component provides tools to manage and display errors and exceptions. + +Resources +--------- + + * [Contributing](https://symfony.com/doc/current/contributing/index.html) + * [Report issues](https://github.com/symfony/symfony/issues) and + [send Pull Requests](https://github.com/symfony/symfony/pulls) + in the [main Symfony repository](https://github.com/symfony/symfony) diff --git a/src/Symfony/Component/ErrorHandler/Tests/DependencyInjection/ErrorHandlerPassTest.php b/src/Symfony/Component/ErrorHandler/Tests/DependencyInjection/ErrorHandlerPassTest.php new file mode 100644 index 0000000000..3d0b8e429f --- /dev/null +++ b/src/Symfony/Component/ErrorHandler/Tests/DependencyInjection/ErrorHandlerPassTest.php @@ -0,0 +1,51 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\ErrorHandler\Tests\DependencyInjection; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\DependencyInjection\Argument\ServiceClosureArgument; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Reference; +use Symfony\Component\DependencyInjection\ServiceLocator; +use Symfony\Component\ErrorHandler\DependencyInjection\ErrorHandlerPass; +use Symfony\Component\ErrorHandler\DependencyInjection\ErrorRenderer; +use Symfony\Component\ErrorHandler\ErrorRenderer\HtmlErrorRenderer; +use Symfony\Component\ErrorHandler\ErrorRenderer\JsonErrorRenderer; + +class ErrorHandlerPassTest extends TestCase +{ + public function testProcess() + { + $container = new ContainerBuilder(); + $container->setParameter('kernel.debug', true); + $definition = $container->register('error_handler.error_renderer', ErrorRenderer::class) + ->addArgument([]) + ; + $container->register('error_handler.renderer.html', HtmlErrorRenderer::class) + ->addTag('error_handler.renderer') + ; + $container->register('error_handler.renderer.json', JsonErrorRenderer::class) + ->addTag('error_handler.renderer') + ; + + (new ErrorHandlerPass())->process($container); + + $serviceLocatorDefinition = $container->getDefinition((string) $definition->getArgument(0)); + $this->assertSame(ServiceLocator::class, $serviceLocatorDefinition->getClass()); + + $expected = [ + 'html' => new ServiceClosureArgument(new Reference('error_handler.renderer.html')), + 'json' => new ServiceClosureArgument(new Reference('error_handler.renderer.json')), + ]; + $this->assertEquals($expected, $serviceLocatorDefinition->getArgument(0)); + } +} diff --git a/src/Symfony/Component/ErrorHandler/Tests/DependencyInjection/ErrorRendererTest.php b/src/Symfony/Component/ErrorHandler/Tests/DependencyInjection/ErrorRendererTest.php new file mode 100644 index 0000000000..fcf909fddc --- /dev/null +++ b/src/Symfony/Component/ErrorHandler/Tests/DependencyInjection/ErrorRendererTest.php @@ -0,0 +1,67 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\ErrorHandler\Tests\DependencyInjection; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\ErrorHandler\DependencyInjection\ErrorRenderer; +use Symfony\Component\ErrorHandler\ErrorRenderer\ErrorRendererInterface; +use Symfony\Component\ErrorHandler\Exception\FlattenException; + +class ErrorRendererTest extends TestCase +{ + /** + * @expectedException \Symfony\Component\ErrorHandler\Exception\ErrorRendererNotFoundException + * @expectedExceptionMessage No error renderer found for format "foo". + */ + public function testInvalidErrorRenderer() + { + $container = $this->getMockBuilder('Psr\Container\ContainerInterface')->getMock(); + $container->expects($this->once())->method('has')->with('foo')->willReturn(false); + + $exception = FlattenException::create(new \Exception('Foo')); + (new ErrorRenderer($container))->render($exception, 'foo'); + } + + public function testCustomErrorRenderer() + { + $container = $this->getMockBuilder('Psr\Container\ContainerInterface')->getMock(); + $container + ->expects($this->once()) + ->method('has') + ->with('foo') + ->willReturn(true) + ; + $container + ->expects($this->once()) + ->method('get') + ->willReturn(new FooErrorRenderer()) + ; + + $errorRenderer = new ErrorRenderer($container); + + $exception = FlattenException::create(new \RuntimeException('Foo')); + $this->assertSame('Foo', $errorRenderer->render($exception, 'foo')); + } +} + +class FooErrorRenderer implements ErrorRendererInterface +{ + public static function getFormat(): string + { + return 'foo'; + } + + public function render(FlattenException $exception): string + { + return $exception->getMessage(); + } +} diff --git a/src/Symfony/Component/Debug/Tests/ErrorHandlerTest.php b/src/Symfony/Component/ErrorHandler/Tests/ErrorHandlerTest.php similarity index 97% rename from src/Symfony/Component/Debug/Tests/ErrorHandlerTest.php rename to src/Symfony/Component/ErrorHandler/Tests/ErrorHandlerTest.php index f758d21e0e..9701558164 100644 --- a/src/Symfony/Component/Debug/Tests/ErrorHandlerTest.php +++ b/src/Symfony/Component/ErrorHandler/Tests/ErrorHandlerTest.php @@ -9,16 +9,16 @@ * file that was distributed with this source code. */ -namespace Symfony\Component\Debug\Tests; +namespace Symfony\Component\ErrorHandler\Tests; use PHPUnit\Framework\TestCase; use Psr\Log\LogLevel; use Psr\Log\NullLogger; -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; +use Symfony\Component\ErrorHandler\BufferingLogger; +use Symfony\Component\ErrorHandler\ErrorHandler; +use Symfony\Component\ErrorHandler\Exception\SilencedErrorContext; +use Symfony\Component\ErrorHandler\Tests\Fixtures\ErrorHandlerThatUsesThePreviousOne; +use Symfony\Component\ErrorHandler\Tests\Fixtures\LoggerThatSetAnErrorHandler; /** * ErrorHandlerTest. @@ -33,7 +33,7 @@ class ErrorHandlerTest extends TestCase $handler = ErrorHandler::register(); try { - $this->assertInstanceOf('Symfony\Component\Debug\ErrorHandler', $handler); + $this->assertInstanceOf('Symfony\Component\ErrorHandler\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_DEPRECATED => [null, LogLevel::INFO], - E_USER_DEPRECATED => [null, LogLevel::INFO], - E_NOTICE => [$logger, LogLevel::WARNING], - E_USER_NOTICE => [$logger, LogLevel::CRITICAL], - 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_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_USER_NOTICE => [$logger, LogLevel::CRITICAL], + E_USER_WARNING => [null, LogLevel::WARNING], + E_WARNING => [null, LogLevel::WARNING], ]; $this->assertSame($loggers, $handler->setLoggers([])); } finally { @@ -375,21 +375,21 @@ class ErrorHandlerTest extends TestCase $handler = new ErrorHandler($bootLogger); $loggers = [ - E_DEPRECATED => [$bootLogger, LogLevel::INFO], - E_USER_DEPRECATED => [$bootLogger, LogLevel::INFO], - E_NOTICE => [$bootLogger, LogLevel::WARNING], - E_USER_NOTICE => [$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_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_USER_NOTICE => [$bootLogger, LogLevel::WARNING], + E_USER_WARNING => [$bootLogger, LogLevel::WARNING], + E_WARNING => [$bootLogger, LogLevel::WARNING], ]; $this->assertSame($loggers, $handler->setLoggers([])); @@ -490,7 +490,7 @@ class ErrorHandlerTest extends TestCase $handler->handleException($exception); - $this->assertInstanceOf('Symfony\Component\Debug\Exception\ClassNotFoundException', $args[0]); + $this->assertInstanceOf('Symfony\Component\ErrorHandler\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/ErrorHandler/Tests/ErrorRenderer/ErrorRendererTest.php b/src/Symfony/Component/ErrorHandler/Tests/ErrorRenderer/ErrorRendererTest.php new file mode 100644 index 0000000000..5f77fdc206 --- /dev/null +++ b/src/Symfony/Component/ErrorHandler/Tests/ErrorRenderer/ErrorRendererTest.php @@ -0,0 +1,62 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\ErrorHandler\Tests\ErrorRenderer; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\ErrorHandler\ErrorRenderer\ErrorRenderer; +use Symfony\Component\ErrorHandler\ErrorRenderer\ErrorRendererInterface; +use Symfony\Component\ErrorHandler\Exception\FlattenException; + +class ErrorRendererTest extends TestCase +{ + /** + * @expectedException \Symfony\Component\ErrorHandler\Exception\ErrorRendererNotFoundException + * @expectedExceptionMessage No error renderer found for format "foo". + */ + public function testErrorRendererNotFound() + { + $exception = FlattenException::create(new \Exception('foo')); + (new ErrorRenderer([]))->render($exception, 'foo'); + } + + /** + * @expectedException \InvalidArgumentException + * @expectedExceptionMessage Error renderer "stdClass" must implement "Symfony\Component\ErrorHandler\ErrorRenderer\ErrorRendererInterface". + */ + public function testInvalidErrorRenderer() + { + $exception = FlattenException::create(new \Exception('foo')); + (new ErrorRenderer([new \stdClass()]))->render($exception, 'foo'); + } + + public function testCustomErrorRenderer() + { + $renderers = [new FooErrorRenderer()]; + $errorRenderer = new ErrorRenderer($renderers); + + $exception = FlattenException::create(new \RuntimeException('Foo')); + $this->assertSame('Foo', $errorRenderer->render($exception, 'foo')); + } +} + +class FooErrorRenderer implements ErrorRendererInterface +{ + public static function getFormat(): string + { + return 'foo'; + } + + public function render(FlattenException $exception): string + { + return $exception->getMessage(); + } +} diff --git a/src/Symfony/Component/ErrorHandler/Tests/ErrorRenderer/HtmlErrorRendererTest.php b/src/Symfony/Component/ErrorHandler/Tests/ErrorRenderer/HtmlErrorRendererTest.php new file mode 100644 index 0000000000..a380de4ab9 --- /dev/null +++ b/src/Symfony/Component/ErrorHandler/Tests/ErrorRenderer/HtmlErrorRendererTest.php @@ -0,0 +1,27 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\ErrorHandler\Tests\ErrorRenderer; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\ErrorHandler\ErrorRenderer\HtmlErrorRenderer; +use Symfony\Component\ErrorHandler\Exception\FlattenException; + +class HtmlErrorRendererTest extends TestCase +{ + public function testRender() + { + $exception = FlattenException::create(new \RuntimeException('Foo')); + $expected = '%A%A%AInternal Server Error%A

Foo

%ARuntimeException%A'; + + $this->assertStringMatchesFormat($expected, (new HtmlErrorRenderer())->render($exception)); + } +} diff --git a/src/Symfony/Component/ErrorHandler/Tests/ErrorRenderer/JsonErrorRendererTest.php b/src/Symfony/Component/ErrorHandler/Tests/ErrorRenderer/JsonErrorRendererTest.php new file mode 100644 index 0000000000..7bfc048402 --- /dev/null +++ b/src/Symfony/Component/ErrorHandler/Tests/ErrorRenderer/JsonErrorRendererTest.php @@ -0,0 +1,27 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\ErrorHandler\Tests\ErrorRenderer; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\ErrorHandler\ErrorRenderer\JsonErrorRenderer; +use Symfony\Component\ErrorHandler\Exception\FlattenException; + +class JsonErrorRendererTest extends TestCase +{ + public function testRender() + { + $exception = FlattenException::create(new \RuntimeException('Foo')); + $expected = '{"title":"Internal Server Error","status":500,"detail":"Foo","exceptions":[{"message":"Foo","class":"RuntimeException","trace":'; + + $this->assertStringStartsWith($expected, (new JsonErrorRenderer())->render($exception)); + } +} diff --git a/src/Symfony/Component/ErrorHandler/Tests/ErrorRenderer/TxtErrorRendererTest.php b/src/Symfony/Component/ErrorHandler/Tests/ErrorRenderer/TxtErrorRendererTest.php new file mode 100644 index 0000000000..d48d924ad9 --- /dev/null +++ b/src/Symfony/Component/ErrorHandler/Tests/ErrorRenderer/TxtErrorRendererTest.php @@ -0,0 +1,27 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\ErrorHandler\Tests\ErrorRenderer; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\ErrorHandler\ErrorRenderer\TxtErrorRenderer; +use Symfony\Component\ErrorHandler\Exception\FlattenException; + +class TxtErrorRendererTest extends TestCase +{ + public function testRender() + { + $exception = FlattenException::create(new \RuntimeException('Foo')); + $expected = '[title] Internal Server Error%A[status] 500%A[detail] Foo%A[1] RuntimeException: Foo%A'; + + $this->assertStringMatchesFormat($expected, (new TxtErrorRenderer())->render($exception)); + } +} diff --git a/src/Symfony/Component/ErrorHandler/Tests/ErrorRenderer/XmlErrorRendererTest.php b/src/Symfony/Component/ErrorHandler/Tests/ErrorRenderer/XmlErrorRendererTest.php new file mode 100644 index 0000000000..4bed569c1b --- /dev/null +++ b/src/Symfony/Component/ErrorHandler/Tests/ErrorRenderer/XmlErrorRendererTest.php @@ -0,0 +1,27 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\ErrorHandler\Tests\ErrorRenderer; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\ErrorHandler\ErrorRenderer\XmlErrorRenderer; +use Symfony\Component\ErrorHandler\Exception\FlattenException; + +class XmlErrorRendererTest extends TestCase +{ + public function testRender() + { + $exception = FlattenException::create(new \RuntimeException('Foo')); + $expected = '%A%AInternal Server Error%A500%AFoo%A'; + + $this->assertStringMatchesFormat($expected, (new XmlErrorRenderer())->render($exception)); + } +} diff --git a/src/Symfony/Component/Debug/Tests/Exception/FlattenExceptionTest.php b/src/Symfony/Component/ErrorHandler/Tests/Exception/FlattenExceptionTest.php similarity index 98% rename from src/Symfony/Component/Debug/Tests/Exception/FlattenExceptionTest.php rename to src/Symfony/Component/ErrorHandler/Tests/Exception/FlattenExceptionTest.php index e86210b903..a9c717e309 100644 --- a/src/Symfony/Component/Debug/Tests/Exception/FlattenExceptionTest.php +++ b/src/Symfony/Component/ErrorHandler/Tests/Exception/FlattenExceptionTest.php @@ -9,11 +9,11 @@ * file that was distributed with this source code. */ -namespace Symfony\Component\Debug\Tests\Exception; +namespace Symfony\Component\ErrorHandler\Tests\Exception; use PHPUnit\Framework\TestCase; -use Symfony\Component\Debug\Exception\FatalThrowableError; -use Symfony\Component\Debug\Exception\FlattenException; +use Symfony\Component\ErrorHandler\Exception\FatalThrowableError; +use Symfony\Component\ErrorHandler\Exception\FlattenException; use Symfony\Component\HttpFoundation\Exception\SuspiciousOperationException; use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; diff --git a/src/Symfony/Component/Debug/Tests/ExceptionHandlerTest.php b/src/Symfony/Component/ErrorHandler/Tests/ExceptionHandlerTest.php similarity index 86% rename from src/Symfony/Component/Debug/Tests/ExceptionHandlerTest.php rename to src/Symfony/Component/ErrorHandler/Tests/ExceptionHandlerTest.php index 4910fe5ec9..03c39d3686 100644 --- a/src/Symfony/Component/Debug/Tests/ExceptionHandlerTest.php +++ b/src/Symfony/Component/ErrorHandler/Tests/ExceptionHandlerTest.php @@ -9,11 +9,11 @@ * file that was distributed with this source code. */ -namespace Symfony\Component\Debug\Tests; +namespace Symfony\Component\ErrorHandler\Tests; use PHPUnit\Framework\TestCase; -use Symfony\Component\Debug\Exception\OutOfMemoryException; -use Symfony\Component\Debug\ExceptionHandler; +use Symfony\Component\ErrorHandler\Exception\OutOfMemoryException; +use Symfony\Component\ErrorHandler\ExceptionHandler; use Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; @@ -39,7 +39,7 @@ class ExceptionHandlerTest extends TestCase $handler->sendPhpResponse(new \RuntimeException('Foo')); $response = ob_get_clean(); - $this->assertContains('Whoops, looks like something went wrong.', $response); + $this->assertContains('The server returned a "500 Internal Server Error".', $response); $this->assertNotContains('
', $response); $handler = new ExceptionHandler(true); @@ -69,7 +69,7 @@ content="0;url=data:text/html;base64,PHNjcmlwdD5hbGVydCgndGVzdDMnKTwvc2NyaXB0Pg" $handler->sendPhpResponse(new NotFoundHttpException('Foo')); $response = ob_get_clean(); - $this->assertContains('Sorry, the page you are looking for could not be found.', $response); + $this->assertContains('The server returned a "404 Not Found".', $response); $expectedHeaders = [ ['HTTP/1.0 404', true, null], @@ -110,7 +110,7 @@ content="0;url=data:text/html;base64,PHNjcmlwdD5hbGVydCgndGVzdDMnKTwvc2NyaXB0Pg" { $exception = new \Exception('foo'); - $handler = $this->getMockBuilder('Symfony\Component\Debug\ExceptionHandler')->setMethods(['sendPhpResponse'])->getMock(); + $handler = $this->getMockBuilder('Symfony\Component\ErrorHandler\ExceptionHandler')->setMethods(['sendPhpResponse'])->getMock(); $handler ->expects($this->exactly(2)) ->method('sendPhpResponse'); @@ -128,7 +128,7 @@ content="0;url=data:text/html;base64,PHNjcmlwdD5hbGVydCgndGVzdDMnKTwvc2NyaXB0Pg" { $exception = new OutOfMemoryException('foo', 0, E_ERROR, __FILE__, __LINE__); - $handler = $this->getMockBuilder('Symfony\Component\Debug\ExceptionHandler')->setMethods(['sendPhpResponse'])->getMock(); + $handler = $this->getMockBuilder('Symfony\Component\ErrorHandler\ExceptionHandler')->setMethods(['sendPhpResponse'])->getMock(); $handler ->expects($this->once()) ->method('sendPhpResponse'); diff --git a/src/Symfony/Component/Debug/Tests/FatalErrorHandler/ClassNotFoundFatalErrorHandlerTest.php b/src/Symfony/Component/ErrorHandler/Tests/FatalErrorHandler/ClassNotFoundFatalErrorHandlerTest.php similarity index 80% rename from src/Symfony/Component/Debug/Tests/FatalErrorHandler/ClassNotFoundFatalErrorHandlerTest.php rename to src/Symfony/Component/ErrorHandler/Tests/FatalErrorHandler/ClassNotFoundFatalErrorHandlerTest.php index 8e615ac640..2fdec8dcf5 100644 --- a/src/Symfony/Component/Debug/Tests/FatalErrorHandler/ClassNotFoundFatalErrorHandlerTest.php +++ b/src/Symfony/Component/ErrorHandler/Tests/FatalErrorHandler/ClassNotFoundFatalErrorHandlerTest.php @@ -9,13 +9,13 @@ * file that was distributed with this source code. */ -namespace Symfony\Component\Debug\Tests\FatalErrorHandler; +namespace Symfony\Component\ErrorHandler\Tests\FatalErrorHandler; use Composer\Autoload\ClassLoader as ComposerClassLoader; use PHPUnit\Framework\TestCase; use Symfony\Component\Debug\DebugClassLoader; -use Symfony\Component\Debug\Exception\FatalErrorException; -use Symfony\Component\Debug\FatalErrorHandler\ClassNotFoundFatalErrorHandler; +use Symfony\Component\ErrorHandler\Exception\FatalErrorException; +use Symfony\Component\ErrorHandler\FatalErrorHandler\ClassNotFoundFatalErrorHandler; class ClassNotFoundFatalErrorHandlerTest extends TestCase { @@ -32,7 +32,7 @@ class ClassNotFoundFatalErrorHandlerTest extends TestCase } if ($function[0] instanceof ComposerClassLoader) { - $function[0]->add('Symfony_Component_Debug_Tests_Fixtures', \dirname(\dirname(\dirname(\dirname(\dirname(__DIR__)))))); + $function[0]->add('Symfony_Component_ErrorHandler_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\Debug\Exception\ClassNotFoundException', $exception); + $this->assertInstanceOf('Symfony\Component\ErrorHandler\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\Debug\Exception\\', realpath(__DIR__.'/../../Exception')); + $autoloader->add('Symfony\Component\ErrorHandler\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 \'UndefinedFunctionException\' not found', + 'message' => 'Class \'UndefinedFuncException\' not found', ], - "Attempted to load class \"UndefinedFunctionException\" from the global namespace.\nDid you forget a \"use\" statement for \"Symfony\Component\Debug\Exception\UndefinedFunctionException\"?", + "Attempted to load class \"UndefinedFuncException\" from the global namespace.\nDid you forget a \"use\" statement for \"Symfony\Component\ErrorHandler\Tests\Fixtures\UndefinedFuncException\"?", ], [ [ @@ -109,7 +109,16 @@ 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_Debug_Tests_Fixtures_PEARClass\"?", + "Attempted to load class \"PEARClass\" from the global namespace.\nDid you forget a \"use\" statement for \"Symfony_Component_ErrorHandler_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\ErrorHandler\Tests\Fixtures\UndefinedFuncException\"?", ], [ [ @@ -118,16 +127,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\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\"?", + "Attempted to load class \"UndefinedFunctionException\" from namespace \"Foo\Bar\".\nDid you forget a \"use\" statement for \"Symfony\Component\ErrorHandler\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\Debug\Exception\UndefinedFunctionException\"?", + "Attempted to load class \"UndefinedFunctionException\" from namespace \"Foo\Bar\".\nDid you forget a \"use\" statement for \"Symfony\Component\ErrorHandler\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\Debug\Exception\ClassNotFoundException', $exception); + $this->assertInstanceOf('Symfony\Component\ErrorHandler\Exception\ClassNotFoundException', $exception); } } diff --git a/src/Symfony/Component/Debug/Tests/FatalErrorHandler/UndefinedFunctionFatalErrorHandlerTest.php b/src/Symfony/Component/ErrorHandler/Tests/FatalErrorHandler/UndefinedFunctionFatalErrorHandlerTest.php similarity index 84% rename from src/Symfony/Component/Debug/Tests/FatalErrorHandler/UndefinedFunctionFatalErrorHandlerTest.php rename to src/Symfony/Component/ErrorHandler/Tests/FatalErrorHandler/UndefinedFunctionFatalErrorHandlerTest.php index de9994e447..c24109b1b3 100644 --- a/src/Symfony/Component/Debug/Tests/FatalErrorHandler/UndefinedFunctionFatalErrorHandlerTest.php +++ b/src/Symfony/Component/ErrorHandler/Tests/FatalErrorHandler/UndefinedFunctionFatalErrorHandlerTest.php @@ -9,11 +9,11 @@ * file that was distributed with this source code. */ -namespace Symfony\Component\Debug\Tests\FatalErrorHandler; +namespace Symfony\Component\ErrorHandler\Tests\FatalErrorHandler; use PHPUnit\Framework\TestCase; -use Symfony\Component\Debug\Exception\FatalErrorException; -use Symfony\Component\Debug\FatalErrorHandler\UndefinedFunctionFatalErrorHandler; +use Symfony\Component\ErrorHandler\Exception\FatalErrorException; +use Symfony\Component\ErrorHandler\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\Debug\Exception\UndefinedFunctionException', $exception); + $this->assertInstanceOf('Symfony\Component\ErrorHandler\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\\debug\\tests\\fatalerrorhandler\\test_namespaced_function\"?", + "Attempted to call function \"test_namespaced_function\" from the global namespace.\nDid you mean to call \"\\symfony\\component\\errorhandler\\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\\debug\\tests\\fatalerrorhandler\\test_namespaced_function\"?", + "Attempted to call function \"test_namespaced_function\" from namespace \"Foo\\Bar\\Baz\".\nDid you mean to call \"\\symfony\\component\\errorhandler\\tests\\fatalerrorhandler\\test_namespaced_function\"?", ], [ [ diff --git a/src/Symfony/Component/Debug/Tests/FatalErrorHandler/UndefinedMethodFatalErrorHandlerTest.php b/src/Symfony/Component/ErrorHandler/Tests/FatalErrorHandler/UndefinedMethodFatalErrorHandlerTest.php similarity index 88% rename from src/Symfony/Component/Debug/Tests/FatalErrorHandler/UndefinedMethodFatalErrorHandlerTest.php rename to src/Symfony/Component/ErrorHandler/Tests/FatalErrorHandler/UndefinedMethodFatalErrorHandlerTest.php index 268a841351..b91792b440 100644 --- a/src/Symfony/Component/Debug/Tests/FatalErrorHandler/UndefinedMethodFatalErrorHandlerTest.php +++ b/src/Symfony/Component/ErrorHandler/Tests/FatalErrorHandler/UndefinedMethodFatalErrorHandlerTest.php @@ -9,11 +9,11 @@ * file that was distributed with this source code. */ -namespace Symfony\Component\Debug\Tests\FatalErrorHandler; +namespace Symfony\Component\ErrorHandler\Tests\FatalErrorHandler; use PHPUnit\Framework\TestCase; -use Symfony\Component\Debug\Exception\FatalErrorException; -use Symfony\Component\Debug\FatalErrorHandler\UndefinedMethodFatalErrorHandler; +use Symfony\Component\ErrorHandler\Exception\FatalErrorException; +use Symfony\Component\ErrorHandler\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\Debug\Exception\UndefinedMethodException', $exception); + $this->assertInstanceOf('Symfony\Component\ErrorHandler\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/Debug/Tests/Fixtures/ErrorHandlerThatUsesThePreviousOne.php b/src/Symfony/Component/ErrorHandler/Tests/Fixtures/ErrorHandlerThatUsesThePreviousOne.php similarity index 88% rename from src/Symfony/Component/Debug/Tests/Fixtures/ErrorHandlerThatUsesThePreviousOne.php rename to src/Symfony/Component/ErrorHandler/Tests/Fixtures/ErrorHandlerThatUsesThePreviousOne.php index d449c40cc7..7cc51ff322 100644 --- a/src/Symfony/Component/Debug/Tests/Fixtures/ErrorHandlerThatUsesThePreviousOne.php +++ b/src/Symfony/Component/ErrorHandler/Tests/Fixtures/ErrorHandlerThatUsesThePreviousOne.php @@ -1,6 +1,6 @@ --EXPECTF-- -object(Symfony\Component\Debug\Exception\ClassNotFoundException)#%d (8) { +object(Symfony\Component\ErrorHandler\Exception\ClassNotFoundException)#%d (8) { ["message":protected]=> - string(131) "Attempted to load class "missing" from namespace "Symfony\Component\Debug". + string(138) "Attempted to load class "missing" from namespace "Symfony\Component\ErrorHandler". Did you forget a "use" statement for another namespace?" ["string":"Exception":private]=> string(0) "" diff --git a/src/Symfony/Component/Debug/Tests/phpt/exception_rethrown.phpt b/src/Symfony/Component/ErrorHandler/Tests/phpt/exception_rethrown.phpt similarity index 94% rename from src/Symfony/Component/Debug/Tests/phpt/exception_rethrown.phpt rename to src/Symfony/Component/ErrorHandler/Tests/phpt/exception_rethrown.phpt index b743d93ad7..82a9006d84 100644 --- a/src/Symfony/Component/Debug/Tests/phpt/exception_rethrown.phpt +++ b/src/Symfony/Component/ErrorHandler/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\Debug\Exception\FatalErrorException)#%d (%d) { +object(Symfony\Component\ErrorHandler\Exception\FatalErrorException)#%d (%d) { ["message":protected]=> - 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)" + string(186) "Error: Class Symfony\Component\ErrorHandler\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/ErrorHandler/composer.json b/src/Symfony/Component/ErrorHandler/composer.json new file mode 100644 index 0000000000..0b90571202 --- /dev/null +++ b/src/Symfony/Component/ErrorHandler/composer.json @@ -0,0 +1,42 @@ +{ + "name": "symfony/error-handler", + "type": "library", + "description": "Symfony ErrorHandler Component", + "keywords": [], + "homepage": "https://symfony.com", + "license": "MIT", + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "require": { + "php": "^7.1.3", + "psr/log": "~1.0" + }, + "require-dev": { + "symfony/debug": "^4.4", + "symfony/dependency-injection": "^4.4", + "symfony/http-kernel": "^4.4" + }, + "conflict": { + "symfony/http-kernel": "<4.4" + }, + "autoload": { + "psr-4": { "Symfony\\Component\\ErrorHandler\\": "" }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "minimum-stability": "dev", + "extra": { + "branch-alias": { + "dev-master": "4.4-dev" + } + } +} diff --git a/src/Symfony/Component/ErrorHandler/phpunit.xml.dist b/src/Symfony/Component/ErrorHandler/phpunit.xml.dist new file mode 100644 index 0000000000..494bb652f4 --- /dev/null +++ b/src/Symfony/Component/ErrorHandler/phpunit.xml.dist @@ -0,0 +1,30 @@ + + + + + + + + + + ./Tests/ + + + + + + ./ + + ./Tests + ./vendor + + + + diff --git a/src/Symfony/Component/HttpKernel/DataCollector/ExceptionDataCollector.php b/src/Symfony/Component/HttpKernel/DataCollector/ExceptionDataCollector.php index c76e7f45bd..9d3b7d0641 100644 --- a/src/Symfony/Component/HttpKernel/DataCollector/ExceptionDataCollector.php +++ b/src/Symfony/Component/HttpKernel/DataCollector/ExceptionDataCollector.php @@ -11,7 +11,7 @@ namespace Symfony\Component\HttpKernel\DataCollector; -use Symfony\Component\Debug\Exception\FlattenException; +use Symfony\Component\ErrorHandler\Exception\FlattenException; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; diff --git a/src/Symfony/Component/HttpKernel/DataCollector/LoggerDataCollector.php b/src/Symfony/Component/HttpKernel/DataCollector/LoggerDataCollector.php index 4091b6760f..a6a3dda5b1 100644 --- a/src/Symfony/Component/HttpKernel/DataCollector/LoggerDataCollector.php +++ b/src/Symfony/Component/HttpKernel/DataCollector/LoggerDataCollector.php @@ -11,7 +11,7 @@ namespace Symfony\Component\HttpKernel\DataCollector; -use Symfony\Component\Debug\Exception\SilencedErrorContext; +use Symfony\Component\ErrorHandler\Exception\SilencedErrorContext; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\RequestStack; use Symfony\Component\HttpFoundation\Response; diff --git a/src/Symfony/Component/HttpKernel/EventListener/DebugHandlersListener.php b/src/Symfony/Component/HttpKernel/EventListener/DebugHandlersListener.php index 493cfb1f4f..71977a1802 100644 --- a/src/Symfony/Component/HttpKernel/EventListener/DebugHandlersListener.php +++ b/src/Symfony/Component/HttpKernel/EventListener/DebugHandlersListener.php @@ -15,8 +15,11 @@ use Psr\Log\LoggerInterface; use Symfony\Component\Console\ConsoleEvents; use Symfony\Component\Console\Event\ConsoleEvent; use Symfony\Component\Console\Output\ConsoleOutputInterface; -use Symfony\Component\Debug\ErrorHandler; -use Symfony\Component\Debug\ExceptionHandler; +use Symfony\Component\ErrorHandler\ErrorHandler; +use Symfony\Component\ErrorHandler\ErrorRenderer\ErrorRenderer; +use Symfony\Component\ErrorHandler\ErrorRenderer\HtmlErrorRenderer; +use Symfony\Component\ErrorHandler\Exception\ErrorRendererNotFoundException; +use Symfony\Component\ErrorHandler\ExceptionHandler; use Symfony\Component\EventDispatcher\Event; use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\HttpFoundation\Request; @@ -43,6 +46,7 @@ class DebugHandlersListener implements EventSubscriberInterface private $fileLinkFormat; private $scope; private $charset; + private $errorRenderer; private $firstCall = true; private $hasTerminatedWithException; @@ -55,7 +59,7 @@ class DebugHandlersListener implements EventSubscriberInterface * @param string|FileLinkFormatter|null $fileLinkFormat The format for links to source files * @param bool $scope Enables/disables scoping mode */ - public function __construct(callable $exceptionHandler = null, LoggerInterface $logger = null, $levels = E_ALL, ?int $throwAt = E_ALL, bool $scream = true, $fileLinkFormat = null, bool $scope = true, string $charset = null) + public function __construct(callable $exceptionHandler = null, LoggerInterface $logger = null, $levels = E_ALL, ?int $throwAt = E_ALL, bool $scream = true, $fileLinkFormat = null, bool $scope = true, string $charset = null, ErrorRenderer $errorRenderer = null) { $this->exceptionHandler = $exceptionHandler; $this->logger = $logger; @@ -65,6 +69,7 @@ class DebugHandlersListener implements EventSubscriberInterface $this->fileLinkFormat = $fileLinkFormat; $this->scope = $scope; $this->charset = $charset; + $this->errorRenderer = $errorRenderer; } /** @@ -162,10 +167,17 @@ class DebugHandlersListener implements EventSubscriberInterface $debug = $this->scream && $this->scope; $controller = function (Request $request) use ($debug) { - $e = $request->attributes->get('exception'); - $handler = new ExceptionHandler($debug, $this->charset, $this->fileLinkFormat); + if (null === $this->errorRenderer) { + $this->errorRenderer = new ErrorRenderer([new HtmlErrorRenderer($debug, $this->charset, $this->fileLinkFormat)]); + } - return new Response($handler->getHtml($e), $e->getStatusCode(), $e->getHeaders()); + $e = $request->attributes->get('exception'); + + try { + return new Response($this->errorRenderer->render($e, $request->getRequestFormat()), $e->getStatusCode(), $e->getHeaders()); + } catch (ErrorRendererNotFoundException $_) { + return new Response($this->errorRenderer->render($e), $e->getStatusCode(), $e->getHeaders()); + } }; (new ExceptionListener($controller, $this->logger, $debug))->onKernelException($event); diff --git a/src/Symfony/Component/HttpKernel/EventListener/ExceptionListener.php b/src/Symfony/Component/HttpKernel/EventListener/ExceptionListener.php index f8044537a8..eb05f7005b 100644 --- a/src/Symfony/Component/HttpKernel/EventListener/ExceptionListener.php +++ b/src/Symfony/Component/HttpKernel/EventListener/ExceptionListener.php @@ -12,11 +12,10 @@ namespace Symfony\Component\HttpKernel\EventListener; use Psr\Log\LoggerInterface; -use Symfony\Component\Debug\Exception\FlattenException; +use Symfony\Component\ErrorHandler\Exception\FlattenException; use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\HttpFoundation\Request; -use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent; use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface; use Symfony\Component\HttpKernel\HttpKernelInterface; diff --git a/src/Symfony/Component/HttpKernel/Tests/DataCollector/ExceptionDataCollectorTest.php b/src/Symfony/Component/HttpKernel/Tests/DataCollector/ExceptionDataCollectorTest.php index 1e8d186cc3..90b50dea39 100644 --- a/src/Symfony/Component/HttpKernel/Tests/DataCollector/ExceptionDataCollectorTest.php +++ b/src/Symfony/Component/HttpKernel/Tests/DataCollector/ExceptionDataCollectorTest.php @@ -12,7 +12,7 @@ namespace Symfony\Component\HttpKernel\Tests\DataCollector; use PHPUnit\Framework\TestCase; -use Symfony\Component\Debug\Exception\FlattenException; +use Symfony\Component\ErrorHandler\Exception\FlattenException; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\DataCollector\ExceptionDataCollector; diff --git a/src/Symfony/Component/HttpKernel/Tests/DataCollector/LoggerDataCollectorTest.php b/src/Symfony/Component/HttpKernel/Tests/DataCollector/LoggerDataCollectorTest.php index 261f098a72..3010c5e02e 100644 --- a/src/Symfony/Component/HttpKernel/Tests/DataCollector/LoggerDataCollectorTest.php +++ b/src/Symfony/Component/HttpKernel/Tests/DataCollector/LoggerDataCollectorTest.php @@ -12,7 +12,7 @@ namespace Symfony\Component\HttpKernel\Tests\DataCollector; use PHPUnit\Framework\TestCase; -use Symfony\Component\Debug\Exception\SilencedErrorContext; +use Symfony\Component\ErrorHandler\Exception\SilencedErrorContext; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\RequestStack; use Symfony\Component\HttpFoundation\Response; @@ -106,7 +106,7 @@ class LoggerDataCollectorTest extends TestCase $logs = array_map(function ($v) { if (isset($v['context']['exception'])) { $e = &$v['context']['exception']; - $e = isset($e["\0*\0message"]) ? [$e["\0*\0message"], $e["\0*\0severity"]] : [$e["\0Symfony\Component\Debug\Exception\SilencedErrorContext\0severity"]]; + $e = isset($e["\0*\0message"]) ? [$e["\0*\0message"], $e["\0*\0severity"]] : [$e["\0Symfony\Component\ErrorHandler\Exception\SilencedErrorContext\0severity"]]; } return $v; diff --git a/src/Symfony/Component/HttpKernel/Tests/EventListener/DebugHandlersListenerTest.php b/src/Symfony/Component/HttpKernel/Tests/EventListener/DebugHandlersListenerTest.php index 1e52bce7a3..746756cf01 100644 --- a/src/Symfony/Component/HttpKernel/Tests/EventListener/DebugHandlersListenerTest.php +++ b/src/Symfony/Component/HttpKernel/Tests/EventListener/DebugHandlersListenerTest.php @@ -19,8 +19,8 @@ use Symfony\Component\Console\Event\ConsoleEvent; use Symfony\Component\Console\Helper\HelperSet; use Symfony\Component\Console\Input\ArgvInput; use Symfony\Component\Console\Output\ConsoleOutput; -use Symfony\Component\Debug\ErrorHandler; -use Symfony\Component\Debug\ExceptionHandler; +use Symfony\Component\ErrorHandler\ErrorHandler; +use Symfony\Component\ErrorHandler\ExceptionHandler; use Symfony\Component\EventDispatcher\EventDispatcher; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Event\KernelEvent; diff --git a/src/Symfony/Component/HttpKernel/composer.json b/src/Symfony/Component/HttpKernel/composer.json index 4d923b7166..23534f5f7c 100644 --- a/src/Symfony/Component/HttpKernel/composer.json +++ b/src/Symfony/Component/HttpKernel/composer.json @@ -17,9 +17,10 @@ ], "require": { "php": "^7.1.3", + "symfony/error-handler": "^4.4", "symfony/event-dispatcher": "^4.3", "symfony/http-foundation": "^4.4|^5.0", - "symfony/debug": "^3.4|^4.0|^5.0", + "symfony/debug": "^4.4|^5.0", "symfony/polyfill-ctype": "^1.8", "symfony/polyfill-php73": "^1.9", "psr/log": "~1.0" diff --git a/src/Symfony/Component/Messenger/EventListener/SendFailedMessageToFailureTransportListener.php b/src/Symfony/Component/Messenger/EventListener/SendFailedMessageToFailureTransportListener.php index d9f607140c..e29979b893 100644 --- a/src/Symfony/Component/Messenger/EventListener/SendFailedMessageToFailureTransportListener.php +++ b/src/Symfony/Component/Messenger/EventListener/SendFailedMessageToFailureTransportListener.php @@ -11,7 +11,7 @@ namespace Symfony\Component\Messenger\EventListener; use Psr\Log\LoggerInterface; -use Symfony\Component\Debug\Exception\FlattenException; +use Symfony\Component\ErrorHandler\Exception\FlattenException; use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\Messenger\Event\WorkerMessageFailedEvent; use Symfony\Component\Messenger\Exception\HandlerFailedException; diff --git a/src/Symfony/Component/Messenger/Stamp/RedeliveryStamp.php b/src/Symfony/Component/Messenger/Stamp/RedeliveryStamp.php index 2895ad9b83..77e4514178 100644 --- a/src/Symfony/Component/Messenger/Stamp/RedeliveryStamp.php +++ b/src/Symfony/Component/Messenger/Stamp/RedeliveryStamp.php @@ -11,7 +11,7 @@ namespace Symfony\Component\Messenger\Stamp; -use Symfony\Component\Debug\Exception\FlattenException; +use Symfony\Component\ErrorHandler\Exception\FlattenException; use Symfony\Component\Messenger\Envelope; /** diff --git a/src/Symfony/Component/Messenger/Tests/Stamp/RedeliveryStampTest.php b/src/Symfony/Component/Messenger/Tests/Stamp/RedeliveryStampTest.php index ebff684b3d..e10a5833ae 100644 --- a/src/Symfony/Component/Messenger/Tests/Stamp/RedeliveryStampTest.php +++ b/src/Symfony/Component/Messenger/Tests/Stamp/RedeliveryStampTest.php @@ -12,7 +12,7 @@ namespace Symfony\Component\Messenger\Tests\Stamp; use PHPUnit\Framework\TestCase; -use Symfony\Component\Debug\Exception\FlattenException; +use Symfony\Component\ErrorHandler\Exception\FlattenException; use Symfony\Component\Messenger\Stamp\RedeliveryStamp; class RedeliveryStampTest extends TestCase diff --git a/src/Symfony/Component/Messenger/composer.json b/src/Symfony/Component/Messenger/composer.json index cf12d9182f..f38e30f1f1 100644 --- a/src/Symfony/Component/Messenger/composer.json +++ b/src/Symfony/Component/Messenger/composer.json @@ -27,7 +27,7 @@ "symfony/dependency-injection": "^3.4.19|^4.1.8|^5.0", "symfony/doctrine-bridge": "^3.4|^4.0|^5.0", "symfony/event-dispatcher": "^4.3|^5.0", - "symfony/http-kernel": "^3.4|^4.0|^5.0", + "symfony/http-kernel": "^4.4|^5.0", "symfony/process": "^3.4|^4.0|^5.0", "symfony/property-access": "^3.4|^4.0|^5.0", "symfony/serializer": "^3.4|^4.0|^5.0", @@ -38,7 +38,8 @@ }, "conflict": { "symfony/event-dispatcher": "<4.3", - "symfony/debug": "<4.1" + "symfony/http-kernel": "<4.4", + "symfony/debug": "<4.4" }, "suggest": { "enqueue/messenger-adapter": "For using the php-enqueue library as a transport." diff --git a/src/Symfony/Component/VarDumper/Caster/ExceptionCaster.php b/src/Symfony/Component/VarDumper/Caster/ExceptionCaster.php index ec168c8da9..60cffcb6b7 100644 --- a/src/Symfony/Component/VarDumper/Caster/ExceptionCaster.php +++ b/src/Symfony/Component/VarDumper/Caster/ExceptionCaster.php @@ -11,7 +11,7 @@ namespace Symfony\Component\VarDumper\Caster; -use Symfony\Component\Debug\Exception\SilencedErrorContext; +use Symfony\Component\ErrorHandler\Exception\SilencedErrorContext; use Symfony\Component\VarDumper\Cloner\Stub; use Symfony\Component\VarDumper\Exception\ThrowingCasterException; diff --git a/src/Symfony/Component/VarDumper/Cloner/AbstractCloner.php b/src/Symfony/Component/VarDumper/Cloner/AbstractCloner.php index ea2b45ffa1..ed16de76bf 100644 --- a/src/Symfony/Component/VarDumper/Cloner/AbstractCloner.php +++ b/src/Symfony/Component/VarDumper/Cloner/AbstractCloner.php @@ -85,7 +85,7 @@ abstract class AbstractCloner implements ClonerInterface 'Symfony\Component\VarDumper\Caster\TraceStub' => ['Symfony\Component\VarDumper\Caster\ExceptionCaster', 'castTraceStub'], 'Symfony\Component\VarDumper\Caster\FrameStub' => ['Symfony\Component\VarDumper\Caster\ExceptionCaster', 'castFrameStub'], 'Symfony\Component\VarDumper\Cloner\AbstractCloner' => ['Symfony\Component\VarDumper\Caster\StubCaster', 'cutInternals'], - 'Symfony\Component\Debug\Exception\SilencedErrorContext' => ['Symfony\Component\VarDumper\Caster\ExceptionCaster', 'castSilencedErrorContext'], + 'Symfony\Component\ErrorHandler\Exception\SilencedErrorContext' => ['Symfony\Component\VarDumper\Caster\ExceptionCaster', 'castSilencedErrorContext'], 'ProxyManager\Proxy\ProxyInterface' => ['Symfony\Component\VarDumper\Caster\ProxyManagerCaster', 'castProxy'], 'PHPUnit_Framework_MockObject_MockObject' => ['Symfony\Component\VarDumper\Caster\StubCaster', 'cutInternals'],