Add ErrorController to preview and render errors

This commit is contained in:
Yonel Ceruto 2019-08-21 02:21:47 -04:00
parent 959eb56488
commit b79532ab0e
38 changed files with 308 additions and 130 deletions

View File

@ -217,9 +217,25 @@ TwigBridge
TwigBundle
----------
* Deprecated default value `twig.controller.exception::showAction` of the `twig.exception_controller` configuration option,
set it to `null` instead. This will also change the default error response format according to https://tools.ietf.org/html/rfc7807
for `json`, `xml`, `atom` and `txt` formats:
* Deprecated `twig.exception_controller` configuration option, set it to "null" and use `framework.error_controller` instead:
Before:
```yaml
twig:
exception_controller: 'App\Controller\MyExceptionController'
```
After:
```yaml
twig:
exception_controller: null
framework:
error_controller: 'App\Controller\MyExceptionController'
```
The new default exception controller will also change the error response content according to
https://tools.ietf.org/html/rfc7807 for `json`, `xml`, `atom` and `txt` formats:
Before:
```json
@ -240,7 +256,8 @@ TwigBundle
}
```
* Deprecated the `ExceptionController` and all built-in error templates, use the error renderer mechanism of the `ErrorRenderer` component
* Deprecated the `ExceptionController` and `PreviewErrorController` controllers, use `ErrorController` from the HttpKernel component instead
* Deprecated all built-in error templates, use the error renderer mechanism of the `ErrorRenderer` component
* Deprecated loading custom error templates in non-html formats. Custom HTML error pages based on Twig keep working as before:
Before (`templates/bundles/TwigBundle/Exception/error.jsonld.twig`):

View File

@ -538,8 +538,8 @@ TwigBundle
* The default value (`false`) of the `twig.strict_variables` configuration option has been changed to `%kernel.debug%`.
* The `transchoice` tag and filter have been removed, use the `trans` ones instead with a `%count%` parameter.
* Removed support for legacy templates directories `src/Resources/views/` and `src/Resources/<BundleName>/views/`, use `templates/` and `templates/bundles/<BundleName>/` instead.
* The default value (`twig.controller.exception::showAction`) of the `twig.exception_controller` configuration option has been changed to `null`.
* Removed `ExceptionController` class and all built-in error templates
* The `twig.exception_controller` configuration option has been removed, use `framework.error_controller` instead.
* Removed `ExceptionController`, `PreviewErrorController` classes and all built-in error templates
TwigBridge
----------

View File

@ -14,7 +14,8 @@ CHANGELOG
* Deprecated `routing.loader.service`, use `routing.loader.container` instead.
* Not tagging service route loaders with `routing.route_loader` has been deprecated.
* Overriding the methods `KernelTestCase::tearDown()` and `WebTestCase::tearDown()` without the `void` return-type is deprecated.
* Added new `error_controller` configuration to handle system exceptions
4.3.0
-----

View File

@ -28,7 +28,6 @@ use Symfony\Component\Lock\Store\SemaphoreStore;
use Symfony\Component\Mailer\Mailer;
use Symfony\Component\Messenger\MessageBusInterface;
use Symfony\Component\PropertyInfo\PropertyInfoExtractorInterface;
use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;
use Symfony\Component\Serializer\Serializer;
use Symfony\Component\Translation\Translator;
use Symfony\Component\Validator\Validation;
@ -84,6 +83,9 @@ class Configuration implements ConfigurationInterface
->beforeNormalization()->ifString()->then(function ($v) { return [$v]; })->end()
->prototype('scalar')->end()
->end()
->scalarNode('error_controller')
->defaultValue('error_controller')
->end()
->end()
;

View File

@ -212,6 +212,7 @@ class FrameworkExtension extends Extension
$container->setParameter('kernel.http_method_override', $config['http_method_override']);
$container->setParameter('kernel.trusted_hosts', $config['trusted_hosts']);
$container->setParameter('kernel.default_locale', $config['default_locale']);
$container->setParameter('kernel.error_controller', $config['error_controller']);
if (!$container->hasParameter('debug.file_link_format')) {
if (!$container->hasParameter('templating.helper.code.file_link_format')) {

View File

@ -21,8 +21,6 @@
<argument>%kernel.debug%</argument>
<argument type="service" id="debug.file_link_formatter" />
<argument>%kernel.debug%</argument>
<argument>%kernel.charset%</argument>
<argument type="service" id="error_renderer" on-invalid="null" />
</service>
<service id="debug.file_link_formatter" class="Symfony\Component\HttpKernel\Debug\FileLinkFormatter">

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8" ?>
<routes xmlns="http://symfony.com/schema/routing"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/routing https://symfony.com/schema/routing/routing-1.0.xsd">
<route id="_preview_error" path="/{code}.{_format}">
<default key="_controller">error_controller::preview</default>
<default key="_format">html</default>
<requirement key="code">\d+</requirement>
</route>
</routes>

View File

@ -41,6 +41,7 @@
<xsd:attribute name="secret" type="xsd:string" />
<xsd:attribute name="default-locale" type="xsd:string" />
<xsd:attribute name="test" type="xsd:boolean" />
<xsd:attribute name="error-controller" type="xsd:string" />
</xsd:complexType>
<xsd:complexType name="form">

View File

@ -88,5 +88,19 @@
<service id="disallow_search_engine_index_response_listener" class="Symfony\Component\HttpKernel\EventListener\DisallowRobotsIndexingListener">
<tag name="kernel.event_subscriber" />
</service>
<service id="error_controller" class="Symfony\Component\HttpKernel\Controller\ErrorController" public="true">
<argument type="service" id="http_kernel" />
<argument>%kernel.error_controller%</argument>
<argument type="service" id="error_renderer" />
</service>
<service id="exception_listener" class="Symfony\Component\HttpKernel\EventListener\ExceptionListener">
<tag name="kernel.event_subscriber" />
<tag name="monolog.logger" channel="request" />
<argument>%kernel.error_controller%</argument>
<argument type="service" id="logger" on-invalid="null" />
<argument>%kernel.debug%</argument>
</service>
</services>
</container>

View File

@ -38,7 +38,7 @@ class TestAppKernel extends Kernel
public function setAnnotatedClassCache(array $annotatedClasses)
{
$annotatedClasses = array_diff($annotatedClasses, ['Symfony\Bundle\WebProfilerBundle\Controller\ExceptionController', 'Symfony\Bundle\TwigBundle\Controller\ExceptionController']);
$annotatedClasses = array_diff($annotatedClasses, ['Symfony\Bundle\WebProfilerBundle\Controller\ExceptionController', 'Symfony\Bundle\TwigBundle\Controller\ExceptionController', 'Symfony\Bundle\TwigBundle\Controller\PreviewErrorController']);
parent::setAnnotatedClassCache($annotatedClasses);
}

View File

@ -373,6 +373,7 @@ class ConfigurationTest extends TestCase
'transports' => [],
'enabled' => !class_exists(FullStack::class) && class_exists(Mailer::class),
],
'error_controller' => 'error_controller',
];
}
}

View File

@ -8,4 +8,4 @@ framework:
twig:
strict_variables: '%kernel.debug%'
exception_controller: ~
exception_controller: null # to be removed in 5.0

View File

@ -7,4 +7,4 @@ framework:
twig:
strict_variables: '%kernel.debug%'
exception_controller: ~
exception_controller: null # to be removed in 5.0

View File

@ -15,13 +15,15 @@ class MissingUserProviderTest extends AbstractWebTestCase
{
public function testUserProviderIsNeeded()
{
$this->expectException('Symfony\Component\Config\Definition\Exception\InvalidConfigurationException');
$this->expectExceptionMessage('"default" firewall requires a user provider but none was defined.');
$client = $this->createClient(['test_case' => 'MissingUserProvider', 'root_config' => 'config.yml']);
$client = $this->createClient(['test_case' => 'MissingUserProvider', 'root_config' => 'config.yml', 'debug' => true]);
$client->request('GET', '/', [], [], [
'PHP_AUTH_USER' => 'username',
'PHP_AUTH_PW' => 'pa$$word',
]);
$response = $client->getResponse();
$this->assertSame(500, $response->getStatusCode());
$this->stringContains('"default" firewall requires a user provider but none was defined.', $response->getContent());
}
}

View File

@ -1,37 +0,0 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Bundle\SecurityBundle\Tests\Functional\app;
use Symfony\Component\ErrorRenderer\ErrorRenderer;
use Symfony\Component\ErrorRenderer\ErrorRenderer\HtmlErrorRenderer;
use Symfony\Component\ErrorRenderer\ErrorRenderer\JsonErrorRenderer;
use Symfony\Component\ErrorRenderer\Exception\FlattenException;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
class ExceptionController
{
private $errorRenderer;
public function __construct()
{
$this->errorRenderer = new ErrorRenderer([
new HtmlErrorRenderer(),
new JsonErrorRenderer(),
]);
}
public function __invoke(Request $request, FlattenException $exception)
{
return new Response($this->errorRenderer->render($exception, $request->getPreferredFormat()), $exception->getStatusCode());
}
}

View File

@ -12,6 +12,5 @@
return [
new Symfony\Bundle\SecurityBundle\SecurityBundle(),
new Symfony\Bundle\FrameworkBundle\FrameworkBundle(),
new Symfony\Bundle\TwigBundle\TwigBundle(),
new Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\JsonLoginBundle\JsonLoginBundle(),
];

View File

@ -1,5 +1,5 @@
imports:
- { resource: ./../config/default.yml }
- { resource: ./../config/framework.yml }
security:
encoders:

View File

@ -1,5 +1,5 @@
imports:
- { resource: ./../config/default.yml }
- { resource: ./../config/framework.yml }
security:
encoders:

View File

@ -2,4 +2,4 @@
twig:
debug: '%kernel.debug%'
strict_variables: '%kernel.debug%'
exception_controller: Symfony\Bundle\SecurityBundle\Tests\Functional\app\ExceptionController
exception_controller: null # to be removed in 5.0

View File

@ -7,8 +7,9 @@ CHANGELOG
* marked the `TemplateIterator` as `internal`
* added HTML comment to beginning and end of `exception_full.html.twig`
* added a new `TwigHtmlErrorRenderer` for `html` format, integrated with the `ErrorRenderer` component
* deprecated `ExceptionController` class and all built-in error templates in favor of the new error renderer mechanism
* deprecated default value `twig.controller.exception::showAction` of `twig.exception_controller` configuration option, set it to `null` instead
* deprecated `ExceptionController` and `PreviewErrorController` controllers, use `ErrorController` from the `HttpKernel` component instead
* deprecated all built-in error templates in favor of the new error renderer mechanism
* deprecated `twig.exception_controller` configuration option, set it to "null" and use `framework.error_controller` configuration instead
4.2.0
-----

View File

@ -19,7 +19,7 @@ use Twig\Environment;
use Twig\Error\LoaderError;
use Twig\Loader\ExistsLoaderInterface;
@trigger_error(sprintf('The "%s" class is deprecated since Symfony 4.4, use the ErrorRenderer component instead.', ExceptionController::class), E_USER_DEPRECATED);
@trigger_error(sprintf('The "%s" class is deprecated since Symfony 4.4, use "%s" instead.', ExceptionController::class, \Symfony\Component\HttpKernel\Controller\ErrorController::class), E_USER_DEPRECATED);
/**
* ExceptionController renders error or exception pages for a given
@ -28,7 +28,7 @@ use Twig\Loader\ExistsLoaderInterface;
* @author Fabien Potencier <fabien@symfony.com>
* @author Matthias Pigulla <mp@webfactory.de>
*
* @deprecated since Symfony 4.4, use the ErrorRenderer component instead.
* @deprecated since Symfony 4.4, use Symfony\Component\HttpKernel\Controller\ErrorController instead.
*/
class ExceptionController
{

View File

@ -11,39 +11,35 @@
namespace Symfony\Bundle\TwigBundle\Controller;
use Symfony\Component\ErrorRenderer\ErrorRenderer;
use Symfony\Component\ErrorRenderer\Exception\FlattenException;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\HttpKernelInterface;
@trigger_error(sprintf('The "%s" class is deprecated since Symfony 4.4, use the "%s" instead.', PreviewErrorController::class, \Symfony\Component\HttpKernel\Controller\ErrorController::class), E_USER_DEPRECATED);
/**
* PreviewErrorController can be used to test error pages.
*
* It will create a test exception and forward it to another controller.
*
* @author Matthias Pigulla <mp@webfactory.de>
*
* @deprecated since Symfony 4.4, use the Symfony\Component\HttpKernel\Controller\ErrorController instead.
*/
class PreviewErrorController
{
protected $kernel;
protected $controller;
private $errorRenderer;
public function __construct(HttpKernelInterface $kernel, $controller, ErrorRenderer $errorRenderer = null)
public function __construct(HttpKernelInterface $kernel, $controller)
{
$this->kernel = $kernel;
$this->controller = $controller;
$this->errorRenderer = $errorRenderer;
}
public function previewErrorPageAction(Request $request, $code)
{
$exception = FlattenException::createFromThrowable(new \Exception('Something has intentionally gone wrong.'), $code, ['X-Debug' => false]);
if (null === $this->controller && null !== $this->errorRenderer) {
return new Response($this->errorRenderer->render($exception, $request->getPreferredFormat()), $code);
}
$exception = FlattenException::createFromThrowable(new \Exception('Something has intentionally gone wrong.'), $code);
/*
* This Request mimics the parameters set by

View File

@ -18,6 +18,8 @@ use Symfony\Component\DependencyInjection\ContainerBuilder;
* Registers the Twig exception listener if Twig is registered as a templating engine.
*
* @author Fabien Potencier <fabien@symfony.com>
*
* @internal
*/
class ExceptionListenerPass implements CompilerPassInterface
{
@ -27,13 +29,18 @@ class ExceptionListenerPass implements CompilerPassInterface
return;
}
// register the exception controller only if Twig is enabled and required dependencies do exist
if (!class_exists('Symfony\Component\ErrorRenderer\Exception\FlattenException') || !interface_exists('Symfony\Component\EventDispatcher\EventSubscriberInterface')) {
// to be removed in 5.0
// register the exception listener only if it's currently used, else use the provided by FrameworkBundle
if (null === $container->getParameter('twig.exception_listener.controller') && $container->hasDefinition('exception_listener')) {
$container->removeDefinition('twig.exception_listener');
} elseif ($container->hasParameter('templating.engines')) {
$engines = $container->getParameter('templating.engines');
if (!\in_array('twig', $engines)) {
$container->removeDefinition('twig.exception_listener');
} else {
$container->removeDefinition('exception_listener');
if ($container->hasParameter('templating.engines')) {
$engines = $container->getParameter('templating.engines');
if (!\in_array('twig', $engines, true)) {
$container->removeDefinition('twig.exception_listener');
}
}
}
}

View File

@ -36,10 +36,18 @@ class Configuration implements ConfigurationInterface
->children()
->scalarNode('exception_controller')
->defaultValue(static function () {
@trigger_error('Relying on the default value ("twig.controller.exception::showAction") of the "twig.exception_controller" configuration option is deprecated since Symfony 4.4, set it to "null" explicitly instead, which will be the new default in 5.0.', E_USER_DEPRECATED);
@trigger_error('The "twig.exception_controller" configuration key has been deprecated in Symfony 4.4, set it to "null" and use "framework.error_controller" configuration key instead.', E_USER_DEPRECATED);
return 'twig.controller.exception::showAction';
})
->validate()
->ifTrue(static function ($v) { return null !== $v; })
->then(static function ($v) {
@trigger_error('The "twig.exception_controller" configuration key has been deprecated in Symfony 4.4, set it to "null" and use "framework.error_controller" configuration key instead.', E_USER_DEPRECATED);
return $v;
})
->end()
->end()
->end()
;

View File

@ -134,6 +134,7 @@
<argument>%twig.exception_listener.controller%</argument>
<argument type="service" id="logger" on-invalid="null" />
<argument>%kernel.debug%</argument>
<deprecated>The "%service_id%" service is deprecated since Symfony 4.4.</deprecated>
</service>
<service id="twig.controller.exception" class="Symfony\Bundle\TwigBundle\Controller\ExceptionController" public="true">
@ -145,7 +146,7 @@
<service id="twig.controller.preview_error" class="Symfony\Bundle\TwigBundle\Controller\PreviewErrorController" public="true">
<argument type="service" id="http_kernel" />
<argument>%twig.exception_listener.controller%</argument>
<argument type="service" id="error_renderer" on-invalid="null" />
<deprecated>The "%service_id%" service is deprecated since Symfony 4.4.</deprecated>
</service>
<service id="twig.configurator.environment" class="Symfony\Bundle\TwigBundle\DependencyInjection\Configurator\EnvironmentConfigurator">

View File

@ -18,6 +18,9 @@ use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\HttpKernelInterface;
/**
* @group legacy
*/
class PreviewErrorControllerTest extends TestCase
{
public function testForwardRequestToConfiguredController()

View File

@ -21,7 +21,7 @@ class ConfigurationTest extends TestCase
{
$input = [
'strict_variables' => false, // to be removed in 5.0 relying on default
'exception_controller' => null, // to be removed in 5.0 relying on default
'exception_controller' => null, // to be removed in 5.0
'form_themes' => ['form_div_layout.html.twig'],
];
@ -45,14 +45,14 @@ class ConfigurationTest extends TestCase
/**
* @group legacy
* @expectedDeprecation Relying on the default value ("twig.controller.exception::showAction") of the "twig.exception_controller" configuration option is deprecated since Symfony 4.4, set it to "null" explicitly instead, which will be the new default in 5.0.
* @expectedDeprecation The "twig.exception_controller" configuration key has been deprecated in Symfony 4.4, set it to "null" and use "framework.error_controller" configuration key instead.
*/
public function testGetExceptionControllerDefault()
{
$processor = new Processor();
$config = $processor->processConfiguration(new Configuration(), [[]]);
$config = $processor->processConfiguration(new Configuration(), [['exception_controller' => 'exception_controller']]);
$this->assertSame('twig.controller.exception::showAction', $config['exception_controller']);
$this->assertSame('exception_controller', $config['exception_controller']);
}
public function testGlobalsAreNotNormalized()

View File

@ -4,5 +4,5 @@ $container->loadFromExtension('twig', [
'autoescape_service' => 'my_project.some_bundle.template_escaping_guesser',
'autoescape_service_method' => 'guess',
'strict_variables' => false, // to be removed in 5.0 relying on default
'exception_controller' => null, // to be removed in 5.0 relying on default
'exception_controller' => null, // to be removed in 5.0
]);

View File

@ -2,5 +2,5 @@
$container->loadFromExtension('twig', [
'strict_variables' => false, // to be removed in 5.0 relying on default
'exception_controller' => null, // to be removed in 5.0 relying on default
'exception_controller' => null, // to be removed in 5.0
]);

View File

@ -12,5 +12,5 @@ $container->loadFromExtension('twig', [
'thousands_separator' => '.',
],
'strict_variables' => false, // to be removed in 5.0 relying on default
'exception_controller' => null, // to be removed in 5.0 relying on default
'exception_controller' => null, // to be removed in 5.0
]);

View File

@ -17,7 +17,7 @@ $container->loadFromExtension('twig', [
'charset' => 'ISO-8859-1',
'debug' => true,
'strict_variables' => true,
'exception_controller' => null,
'exception_controller' => null, // to be removed in 5.0
'default_path' => '%kernel.project_dir%/Fixtures/templates',
'paths' => [
'path1',

View File

@ -32,7 +32,7 @@ class TwigExtensionTest extends TestCase
$container->registerExtension(new TwigExtension());
$container->loadFromExtension('twig', [
'strict_variables' => false, // to be removed in 5.0 relying on default
'exception_controller' => null, // to be removed in 5.0 relying on default
'exception_controller' => null, // to be removed in 5.0
]);
$this->compileContainer($container);

View File

@ -68,7 +68,7 @@ class NoTemplatingEntryKernel extends Kernel
])
->loadFromExtension('twig', [
'strict_variables' => false, // to be removed in 5.0 relying on default
'exception_controller' => null, // to be removed in 5.0 relying on default
'exception_controller' => null, // to be removed in 5.0
'default_path' => __DIR__.'/templates',
])
;

View File

@ -13,6 +13,7 @@ CHANGELOG
current directory or with a glob pattern. The fallback directories have never been advocated
so you likely do not use those in any app based on the SF Standard or Flex edition.
* Marked all dispatched event classes as `@final`
* Added `ErrorController` to enable the preview and error rendering mechanism
4.3.0
-----

View File

@ -0,0 +1,67 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpKernel\Controller;
use Symfony\Component\ErrorRenderer\ErrorRenderer;
use Symfony\Component\ErrorRenderer\Exception\ErrorRendererNotFoundException;
use Symfony\Component\ErrorRenderer\Exception\FlattenException;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\HttpKernelInterface;
/**
* Renders error or exception pages from a given FlattenException.
*
* @author Yonel Ceruto <yonelceruto@gmail.com>
* @author Matthias Pigulla <mp@webfactory.de>
*/
class ErrorController
{
private $kernel;
private $controller;
private $errorRenderer;
public function __construct(HttpKernelInterface $kernel, $controller, ErrorRenderer $errorRenderer)
{
$this->kernel = $kernel;
$this->controller = $controller;
$this->errorRenderer = $errorRenderer;
}
public function __invoke(Request $request, FlattenException $exception): Response
{
try {
return new Response($this->errorRenderer->render($exception, $request->getPreferredFormat()), $exception->getStatusCode(), $exception->getHeaders());
} catch (ErrorRendererNotFoundException $e) {
return new Response($this->errorRenderer->render($exception), $exception->getStatusCode(), $exception->getHeaders());
}
}
public function preview(Request $request, int $code): Response
{
$exception = FlattenException::createFromThrowable(new \Exception('This is a sample exception.'), $code, ['X-Debug' => false]);
/*
* This Request mimics the parameters set by
* \Symfony\Component\HttpKernel\EventListener\ExceptionListener::duplicateRequest, with
* the additional "showException" flag.
*/
$subRequest = $request->duplicate(null, null, [
'_controller' => $this->controller,
'exception' => $exception,
'logger' => null,
'showException' => false,
]);
return $this->kernel->handle($subRequest, HttpKernelInterface::SUB_REQUEST);
}
}

View File

@ -16,15 +16,9 @@ use Symfony\Component\Console\ConsoleEvents;
use Symfony\Component\Console\Event\ConsoleEvent;
use Symfony\Component\Console\Output\ConsoleOutputInterface;
use Symfony\Component\ErrorHandler\ErrorHandler;
use Symfony\Component\ErrorRenderer\ErrorRenderer;
use Symfony\Component\ErrorRenderer\ErrorRenderer\HtmlErrorRenderer;
use Symfony\Component\ErrorRenderer\Exception\ErrorRendererNotFoundException;
use Symfony\Component\EventDispatcher\Event;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Debug\FileLinkFormatter;
use Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent;
use Symfony\Component\HttpKernel\Event\KernelEvent;
use Symfony\Component\HttpKernel\KernelEvents;
@ -44,8 +38,6 @@ class DebugHandlersListener implements EventSubscriberInterface
private $scream;
private $fileLinkFormat;
private $scope;
private $charset;
private $errorRenderer;
private $firstCall = true;
private $hasTerminatedWithException;
@ -57,7 +49,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, ErrorRenderer $errorRenderer = 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)
{
$this->exceptionHandler = $exceptionHandler;
$this->logger = $logger;
@ -66,8 +58,6 @@ class DebugHandlersListener implements EventSubscriberInterface
$this->scream = $scream;
$this->fileLinkFormat = $fileLinkFormat;
$this->scope = $scope;
$this->charset = $charset;
$this->errorRenderer = $errorRenderer;
}
/**
@ -142,33 +132,6 @@ class DebugHandlersListener implements EventSubscriberInterface
}
}
/**
* @internal
*/
public function onKernelException(GetResponseForExceptionEvent $event)
{
if (!$this->hasTerminatedWithException || !$event->isMasterRequest()) {
return;
}
$debug = $this->scream && $this->scope;
$controller = function (Request $request) use ($debug) {
if (null === $this->errorRenderer) {
$this->errorRenderer = new ErrorRenderer([new HtmlErrorRenderer($debug, $this->charset, $this->fileLinkFormat)]);
}
$e = $request->attributes->get('exception');
try {
return new Response($this->errorRenderer->render($e, $request->getPreferredFormat()), $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);
}
public static function getSubscribedEvents()
{
$events = [KernelEvents::REQUEST => ['configure', 2048]];
@ -177,8 +140,6 @@ class DebugHandlersListener implements EventSubscriberInterface
$events[ConsoleEvents::COMMAND] = ['configure', 2048];
}
$events[KernelEvents::EXCEPTION] = ['onKernelException', -2048];
return $events;
}
}

View File

@ -0,0 +1,123 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpKernel\Tests\Controller;
use PHPUnit\Framework\TestCase;
use Symfony\Component\ErrorRenderer\ErrorRenderer;
use Symfony\Component\ErrorRenderer\ErrorRenderer\HtmlErrorRenderer;
use Symfony\Component\ErrorRenderer\ErrorRenderer\JsonErrorRenderer;
use Symfony\Component\ErrorRenderer\Exception\FlattenException;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Controller\ErrorController;
use Symfony\Component\HttpKernel\Exception\HttpException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\HttpKernel\HttpKernelInterface;
class ErrorControllerTest extends TestCase
{
/**
* @dataProvider getInvokeControllerDataProvider
*/
public function testInvokeController(Request $request, FlattenException $exception, int $statusCode, string $content)
{
$kernel = $this->getMockBuilder(HttpKernelInterface::class)->getMock();
$errorRenderer = new ErrorRenderer([new HtmlErrorRenderer(), new JsonErrorRenderer()]);
$controller = new ErrorController($kernel, null, $errorRenderer);
$response = $controller($request, $exception);
$this->assertSame($statusCode, $response->getStatusCode());
self::assertStringContainsString($content, strtr($response->getContent(), ["\n" => '', ' ' => '']));
}
public function getInvokeControllerDataProvider()
{
yield 'default status code and HTML format' => [
new Request(),
FlattenException::createFromThrowable(new \Exception()),
500,
'The server returned a "500 Internal Server Error".',
];
yield 'custom status code' => [
new Request(),
FlattenException::createFromThrowable(new NotFoundHttpException('Page not found.')),
404,
'The server returned a "404 Not Found".',
];
$request = new Request();
$request->attributes->set('_format', 'json');
yield 'custom format via _format attribute' => [
$request,
FlattenException::createFromThrowable(new \Exception('foo')),
500,
'{"title": "Internal Server Error","status": 500,"detail": "foo"}',
];
$request = new Request();
$request->headers->set('Accept', 'application/json');
yield 'custom format via Accept header' => [
$request,
FlattenException::createFromThrowable(new HttpException(405, 'Invalid request.')),
405,
'{"title": "Method Not Allowed","status": 405,"detail": "Invalid request."}',
];
$request = new Request();
$request->headers->set('Content-Type', 'application/json');
yield 'custom format via Content-Type header' => [
$request,
FlattenException::createFromThrowable(new HttpException(405, 'Invalid request.')),
405,
'{"title": "Method Not Allowed","status": 405,"detail": "Invalid request."}',
];
$request = new Request();
$request->attributes->set('_format', 'unknown');
yield 'default HTML format for unknown formats' => [
$request,
FlattenException::createFromThrowable(new HttpException(405, 'Invalid request.')),
405,
'The server returned a "405 Method Not Allowed".',
];
}
public function testPreviewController()
{
$_controller = 'error_controller';
$code = 404;
$kernel = $this->getMockBuilder(HttpKernelInterface::class)->getMock();
$kernel
->expects($this->once())
->method('handle')
->with(
$this->callback(function (Request $request) use ($_controller, $code) {
$exception = $request->attributes->get('exception');
$this->assertSame($_controller, $request->attributes->get('_controller'));
$this->assertInstanceOf(FlattenException::class, $exception);
$this->assertSame($code, $exception->getStatusCode());
$this->assertFalse($request->attributes->get('showException'));
return true;
}),
$this->equalTo(HttpKernelInterface::SUB_REQUEST)
)
->willReturn($response = new Response());
$controller = new ErrorController($kernel, $_controller, new ErrorRenderer([]));
$this->assertSame($response, $controller->preview(new Request(), $code));
}
}

View File

@ -101,7 +101,6 @@ class DebugHandlersListenerTest extends TestCase
$xListeners = [
KernelEvents::REQUEST => [[$listener, 'configure']],
ConsoleEvents::COMMAND => [[$listener, 'configure']],
KernelEvents::EXCEPTION => [[$listener, 'onKernelException']],
];
$this->assertSame($xListeners, $dispatcher->getListeners());