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 TwigBundle
---------- ----------
* Deprecated default value `twig.controller.exception::showAction` of the `twig.exception_controller` configuration option, * Deprecated `twig.exception_controller` configuration option, set it to "null" and use `framework.error_controller` instead:
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: 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: Before:
```json ```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: * 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`): 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 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. * 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. * 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`. * The `twig.exception_controller` configuration option has been removed, use `framework.error_controller` instead.
* Removed `ExceptionController` class and all built-in error templates * Removed `ExceptionController`, `PreviewErrorController` classes and all built-in error templates
TwigBridge TwigBridge
---------- ----------

View File

@ -14,7 +14,8 @@ CHANGELOG
* Deprecated `routing.loader.service`, use `routing.loader.container` instead. * Deprecated `routing.loader.service`, use `routing.loader.container` instead.
* Not tagging service route loaders with `routing.route_loader` has been deprecated. * 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. * 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 4.3.0
----- -----

View File

@ -28,7 +28,6 @@ use Symfony\Component\Lock\Store\SemaphoreStore;
use Symfony\Component\Mailer\Mailer; use Symfony\Component\Mailer\Mailer;
use Symfony\Component\Messenger\MessageBusInterface; use Symfony\Component\Messenger\MessageBusInterface;
use Symfony\Component\PropertyInfo\PropertyInfoExtractorInterface; use Symfony\Component\PropertyInfo\PropertyInfoExtractorInterface;
use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;
use Symfony\Component\Serializer\Serializer; use Symfony\Component\Serializer\Serializer;
use Symfony\Component\Translation\Translator; use Symfony\Component\Translation\Translator;
use Symfony\Component\Validator\Validation; use Symfony\Component\Validator\Validation;
@ -84,6 +83,9 @@ class Configuration implements ConfigurationInterface
->beforeNormalization()->ifString()->then(function ($v) { return [$v]; })->end() ->beforeNormalization()->ifString()->then(function ($v) { return [$v]; })->end()
->prototype('scalar')->end() ->prototype('scalar')->end()
->end() ->end()
->scalarNode('error_controller')
->defaultValue('error_controller')
->end()
->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.http_method_override', $config['http_method_override']);
$container->setParameter('kernel.trusted_hosts', $config['trusted_hosts']); $container->setParameter('kernel.trusted_hosts', $config['trusted_hosts']);
$container->setParameter('kernel.default_locale', $config['default_locale']); $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('debug.file_link_format')) {
if (!$container->hasParameter('templating.helper.code.file_link_format')) { if (!$container->hasParameter('templating.helper.code.file_link_format')) {

View File

@ -21,8 +21,6 @@
<argument>%kernel.debug%</argument> <argument>%kernel.debug%</argument>
<argument type="service" id="debug.file_link_formatter" /> <argument type="service" id="debug.file_link_formatter" />
<argument>%kernel.debug%</argument> <argument>%kernel.debug%</argument>
<argument>%kernel.charset%</argument>
<argument type="service" id="error_renderer" on-invalid="null" />
</service> </service>
<service id="debug.file_link_formatter" class="Symfony\Component\HttpKernel\Debug\FileLinkFormatter"> <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="secret" type="xsd:string" />
<xsd:attribute name="default-locale" type="xsd:string" /> <xsd:attribute name="default-locale" type="xsd:string" />
<xsd:attribute name="test" type="xsd:boolean" /> <xsd:attribute name="test" type="xsd:boolean" />
<xsd:attribute name="error-controller" type="xsd:string" />
</xsd:complexType> </xsd:complexType>
<xsd:complexType name="form"> <xsd:complexType name="form">

View File

@ -88,5 +88,19 @@
<service id="disallow_search_engine_index_response_listener" class="Symfony\Component\HttpKernel\EventListener\DisallowRobotsIndexingListener"> <service id="disallow_search_engine_index_response_listener" class="Symfony\Component\HttpKernel\EventListener\DisallowRobotsIndexingListener">
<tag name="kernel.event_subscriber" /> <tag name="kernel.event_subscriber" />
</service> </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> </services>
</container> </container>

View File

@ -38,7 +38,7 @@ class TestAppKernel extends Kernel
public function setAnnotatedClassCache(array $annotatedClasses) 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); parent::setAnnotatedClassCache($annotatedClasses);
} }

View File

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

View File

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

View File

@ -7,4 +7,4 @@ framework:
twig: twig:
strict_variables: '%kernel.debug%' 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() public function testUserProviderIsNeeded()
{ {
$this->expectException('Symfony\Component\Config\Definition\Exception\InvalidConfigurationException'); $client = $this->createClient(['test_case' => 'MissingUserProvider', 'root_config' => 'config.yml', 'debug' => true]);
$this->expectExceptionMessage('"default" firewall requires a user provider but none was defined.');
$client = $this->createClient(['test_case' => 'MissingUserProvider', 'root_config' => 'config.yml']);
$client->request('GET', '/', [], [], [ $client->request('GET', '/', [], [], [
'PHP_AUTH_USER' => 'username', 'PHP_AUTH_USER' => 'username',
'PHP_AUTH_PW' => 'pa$$word', '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 [ return [
new Symfony\Bundle\SecurityBundle\SecurityBundle(), new Symfony\Bundle\SecurityBundle\SecurityBundle(),
new Symfony\Bundle\FrameworkBundle\FrameworkBundle(), new Symfony\Bundle\FrameworkBundle\FrameworkBundle(),
new Symfony\Bundle\TwigBundle\TwigBundle(),
new Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\JsonLoginBundle\JsonLoginBundle(), new Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\JsonLoginBundle\JsonLoginBundle(),
]; ];

View File

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

View File

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

View File

@ -2,4 +2,4 @@
twig: twig:
debug: '%kernel.debug%' debug: '%kernel.debug%'
strict_variables: '%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` * marked the `TemplateIterator` as `internal`
* added HTML comment to beginning and end of `exception_full.html.twig` * added HTML comment to beginning and end of `exception_full.html.twig`
* added a new `TwigHtmlErrorRenderer` for `html` format, integrated with the `ErrorRenderer` component * 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 `ExceptionController` and `PreviewErrorController` controllers, use `ErrorController` from the `HttpKernel` component instead
* deprecated default value `twig.controller.exception::showAction` of `twig.exception_controller` configuration option, set it to `null` 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 4.2.0
----- -----

View File

@ -19,7 +19,7 @@ use Twig\Environment;
use Twig\Error\LoaderError; use Twig\Error\LoaderError;
use Twig\Loader\ExistsLoaderInterface; 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 * ExceptionController renders error or exception pages for a given
@ -28,7 +28,7 @@ use Twig\Loader\ExistsLoaderInterface;
* @author Fabien Potencier <fabien@symfony.com> * @author Fabien Potencier <fabien@symfony.com>
* @author Matthias Pigulla <mp@webfactory.de> * @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 class ExceptionController
{ {

View File

@ -11,39 +11,35 @@
namespace Symfony\Bundle\TwigBundle\Controller; namespace Symfony\Bundle\TwigBundle\Controller;
use Symfony\Component\ErrorRenderer\ErrorRenderer;
use Symfony\Component\ErrorRenderer\Exception\FlattenException; use Symfony\Component\ErrorRenderer\Exception\FlattenException;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\HttpKernelInterface; 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. * PreviewErrorController can be used to test error pages.
* *
* It will create a test exception and forward it to another controller. * It will create a test exception and forward it to another controller.
* *
* @author Matthias Pigulla <mp@webfactory.de> * @author Matthias Pigulla <mp@webfactory.de>
*
* @deprecated since Symfony 4.4, use the Symfony\Component\HttpKernel\Controller\ErrorController instead.
*/ */
class PreviewErrorController class PreviewErrorController
{ {
protected $kernel; protected $kernel;
protected $controller; protected $controller;
private $errorRenderer;
public function __construct(HttpKernelInterface $kernel, $controller, ErrorRenderer $errorRenderer = null) public function __construct(HttpKernelInterface $kernel, $controller)
{ {
$this->kernel = $kernel; $this->kernel = $kernel;
$this->controller = $controller; $this->controller = $controller;
$this->errorRenderer = $errorRenderer;
} }
public function previewErrorPageAction(Request $request, $code) public function previewErrorPageAction(Request $request, $code)
{ {
$exception = FlattenException::createFromThrowable(new \Exception('Something has intentionally gone wrong.'), $code, ['X-Debug' => false]); $exception = FlattenException::createFromThrowable(new \Exception('Something has intentionally gone wrong.'), $code);
if (null === $this->controller && null !== $this->errorRenderer) {
return new Response($this->errorRenderer->render($exception, $request->getPreferredFormat()), $code);
}
/* /*
* This Request mimics the parameters set by * 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. * Registers the Twig exception listener if Twig is registered as a templating engine.
* *
* @author Fabien Potencier <fabien@symfony.com> * @author Fabien Potencier <fabien@symfony.com>
*
* @internal
*/ */
class ExceptionListenerPass implements CompilerPassInterface class ExceptionListenerPass implements CompilerPassInterface
{ {
@ -27,13 +29,18 @@ class ExceptionListenerPass implements CompilerPassInterface
return; return;
} }
// register the exception controller only if Twig is enabled and required dependencies do exist // to be removed in 5.0
if (!class_exists('Symfony\Component\ErrorRenderer\Exception\FlattenException') || !interface_exists('Symfony\Component\EventDispatcher\EventSubscriberInterface')) { // 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'); $container->removeDefinition('twig.exception_listener');
} elseif ($container->hasParameter('templating.engines')) { } else {
$engines = $container->getParameter('templating.engines'); $container->removeDefinition('exception_listener');
if (!\in_array('twig', $engines)) {
$container->removeDefinition('twig.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() ->children()
->scalarNode('exception_controller') ->scalarNode('exception_controller')
->defaultValue(static function () { ->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'; 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()
->end() ->end()
; ;

View File

@ -134,6 +134,7 @@
<argument>%twig.exception_listener.controller%</argument> <argument>%twig.exception_listener.controller%</argument>
<argument type="service" id="logger" on-invalid="null" /> <argument type="service" id="logger" on-invalid="null" />
<argument>%kernel.debug%</argument> <argument>%kernel.debug%</argument>
<deprecated>The "%service_id%" service is deprecated since Symfony 4.4.</deprecated>
</service> </service>
<service id="twig.controller.exception" class="Symfony\Bundle\TwigBundle\Controller\ExceptionController" public="true"> <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"> <service id="twig.controller.preview_error" class="Symfony\Bundle\TwigBundle\Controller\PreviewErrorController" public="true">
<argument type="service" id="http_kernel" /> <argument type="service" id="http_kernel" />
<argument>%twig.exception_listener.controller%</argument> <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>
<service id="twig.configurator.environment" class="Symfony\Bundle\TwigBundle\DependencyInjection\Configurator\EnvironmentConfigurator"> <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\HttpFoundation\Response;
use Symfony\Component\HttpKernel\HttpKernelInterface; use Symfony\Component\HttpKernel\HttpKernelInterface;
/**
* @group legacy
*/
class PreviewErrorControllerTest extends TestCase class PreviewErrorControllerTest extends TestCase
{ {
public function testForwardRequestToConfiguredController() public function testForwardRequestToConfiguredController()

View File

@ -21,7 +21,7 @@ class ConfigurationTest extends TestCase
{ {
$input = [ $input = [
'strict_variables' => false, // to be removed in 5.0 relying on default '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'], 'form_themes' => ['form_div_layout.html.twig'],
]; ];
@ -45,14 +45,14 @@ class ConfigurationTest extends TestCase
/** /**
* @group legacy * @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() public function testGetExceptionControllerDefault()
{ {
$processor = new Processor(); $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() public function testGlobalsAreNotNormalized()

View File

@ -4,5 +4,5 @@ $container->loadFromExtension('twig', [
'autoescape_service' => 'my_project.some_bundle.template_escaping_guesser', 'autoescape_service' => 'my_project.some_bundle.template_escaping_guesser',
'autoescape_service_method' => 'guess', 'autoescape_service_method' => 'guess',
'strict_variables' => false, // to be removed in 5.0 relying on default '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', [ $container->loadFromExtension('twig', [
'strict_variables' => false, // to be removed in 5.0 relying on default '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' => '.', 'thousands_separator' => '.',
], ],
'strict_variables' => false, // to be removed in 5.0 relying on default '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', 'charset' => 'ISO-8859-1',
'debug' => true, 'debug' => true,
'strict_variables' => true, 'strict_variables' => true,
'exception_controller' => null, 'exception_controller' => null, // to be removed in 5.0
'default_path' => '%kernel.project_dir%/Fixtures/templates', 'default_path' => '%kernel.project_dir%/Fixtures/templates',
'paths' => [ 'paths' => [
'path1', 'path1',

View File

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

View File

@ -68,7 +68,7 @@ class NoTemplatingEntryKernel extends Kernel
]) ])
->loadFromExtension('twig', [ ->loadFromExtension('twig', [
'strict_variables' => false, // to be removed in 5.0 relying on default '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', '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 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. 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` * Marked all dispatched event classes as `@final`
* Added `ErrorController` to enable the preview and error rendering mechanism
4.3.0 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\Event\ConsoleEvent;
use Symfony\Component\Console\Output\ConsoleOutputInterface; use Symfony\Component\Console\Output\ConsoleOutputInterface;
use Symfony\Component\ErrorHandler\ErrorHandler; 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\Event;
use Symfony\Component\EventDispatcher\EventSubscriberInterface; 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\Debug\FileLinkFormatter;
use Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent;
use Symfony\Component\HttpKernel\Event\KernelEvent; use Symfony\Component\HttpKernel\Event\KernelEvent;
use Symfony\Component\HttpKernel\KernelEvents; use Symfony\Component\HttpKernel\KernelEvents;
@ -44,8 +38,6 @@ class DebugHandlersListener implements EventSubscriberInterface
private $scream; private $scream;
private $fileLinkFormat; private $fileLinkFormat;
private $scope; private $scope;
private $charset;
private $errorRenderer;
private $firstCall = true; private $firstCall = true;
private $hasTerminatedWithException; private $hasTerminatedWithException;
@ -57,7 +49,7 @@ class DebugHandlersListener implements EventSubscriberInterface
* @param string|FileLinkFormatter|null $fileLinkFormat The format for links to source files * @param string|FileLinkFormatter|null $fileLinkFormat The format for links to source files
* @param bool $scope Enables/disables scoping mode * @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->exceptionHandler = $exceptionHandler;
$this->logger = $logger; $this->logger = $logger;
@ -66,8 +58,6 @@ class DebugHandlersListener implements EventSubscriberInterface
$this->scream = $scream; $this->scream = $scream;
$this->fileLinkFormat = $fileLinkFormat; $this->fileLinkFormat = $fileLinkFormat;
$this->scope = $scope; $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() public static function getSubscribedEvents()
{ {
$events = [KernelEvents::REQUEST => ['configure', 2048]]; $events = [KernelEvents::REQUEST => ['configure', 2048]];
@ -177,8 +140,6 @@ class DebugHandlersListener implements EventSubscriberInterface
$events[ConsoleEvents::COMMAND] = ['configure', 2048]; $events[ConsoleEvents::COMMAND] = ['configure', 2048];
} }
$events[KernelEvents::EXCEPTION] = ['onKernelException', -2048];
return $events; 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 = [ $xListeners = [
KernelEvents::REQUEST => [[$listener, 'configure']], KernelEvents::REQUEST => [[$listener, 'configure']],
ConsoleEvents::COMMAND => [[$listener, 'configure']], ConsoleEvents::COMMAND => [[$listener, 'configure']],
KernelEvents::EXCEPTION => [[$listener, 'onKernelException']],
]; ];
$this->assertSame($xListeners, $dispatcher->getListeners()); $this->assertSame($xListeners, $dispatcher->getListeners());