diff --git a/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md b/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md
index 5e29298a6a3ff..189946dcd8705 100644
--- a/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md
+++ b/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md
@@ -11,6 +11,7 @@ CHANGELOG
* Deprecate the `cache.adapter.doctrine` service
* Add support for resetting container services after each messenger message
* Add `configureContainer()`, `configureRoutes()`, `getConfigDir()` and `getBundlesPath()` to `MicroKernelTrait`
+ * Add support for configuring log level, and status code by exception class
5.3
---
diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php
index 5750dd6de3b69..4d1d2d6e1ccb6 100644
--- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php
+++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php
@@ -15,6 +15,7 @@
use Doctrine\Common\Annotations\PsrCachedReader;
use Doctrine\Common\Cache\Cache;
use Doctrine\DBAL\Connection;
+use Psr\Log\LogLevel;
use Symfony\Bundle\FullStack;
use Symfony\Component\Asset\Package;
use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition;
@@ -139,6 +140,7 @@ public function getConfigTreeBuilder()
$this->addPropertyInfoSection($rootNode, $enableIfStandalone);
$this->addCacheSection($rootNode, $willBeAvailable);
$this->addPhpErrorsSection($rootNode);
+ $this->addExceptionsSection($rootNode);
$this->addWebLinkSection($rootNode, $enableIfStandalone);
$this->addLockSection($rootNode, $enableIfStandalone);
$this->addMessengerSection($rootNode, $enableIfStandalone);
@@ -1163,6 +1165,64 @@ private function addPhpErrorsSection(ArrayNodeDefinition $rootNode)
;
}
+ private function addExceptionsSection(ArrayNodeDefinition $rootNode)
+ {
+ $logLevels = (new \ReflectionClass(LogLevel::class))->getConstants();
+
+ $rootNode
+ ->children()
+ ->arrayNode('exceptions')
+ ->info('Exception handling configuration')
+ ->beforeNormalization()
+ ->ifArray()
+ ->then(function (array $v): array {
+ if (!\array_key_exists('exception', $v)) {
+ return $v;
+ }
+
+ // Fix XML normalization
+ $data = isset($v['exception'][0]) ? $v['exception'] : [$v['exception']];
+ $exceptions = [];
+ foreach ($data as $exception) {
+ $config = [];
+ if (\array_key_exists('log-level', $exception)) {
+ $config['log_level'] = $exception['log-level'];
+ }
+ if (\array_key_exists('status-code', $exception)) {
+ $config['status_code'] = $exception['status-code'];
+ }
+ $exceptions[$exception['name']] = $config;
+ }
+
+ return $exceptions;
+ })
+ ->end()
+ ->prototype('array')
+ ->fixXmlConfig('exception')
+ ->children()
+ ->scalarNode('log_level')
+ ->info('The level of log message. Null to let Symfony decide.')
+ ->validate()
+ ->ifTrue(function ($v) use ($logLevels) { return !\in_array($v, $logLevels); })
+ ->thenInvalid(sprintf('The log level is not valid. Pick one among "%s".', implode('", "', $logLevels)))
+ ->end()
+ ->defaultNull()
+ ->end()
+ ->scalarNode('status_code')
+ ->info('The status code of the response. Null to let Symfony decide.')
+ ->validate()
+ ->ifTrue(function ($v) { return !\in_array($v, range(100, 499)); })
+ ->thenInvalid('The log level is not valid. Pick one among between 100 et 599.')
+ ->end()
+ ->defaultNull()
+ ->end()
+ ->end()
+ ->end()
+ ->end()
+ ->end()
+ ;
+ }
+
private function addLockSection(ArrayNodeDefinition $rootNode, callable $enableIfStandalone)
{
$rootNode
diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php
index cd4d45ff4c015..84299bd0ffc09 100644
--- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php
+++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php
@@ -427,6 +427,8 @@ public function load(array $configs, ContainerBuilder $container)
$this->registerPropertyAccessConfiguration($config['property_access'], $container, $loader);
$this->registerSecretsConfiguration($config['secrets'], $container, $loader);
+ $container->getDefinition('exception_listener')->replaceArgument(3, $config['exceptions']);
+
if ($this->isConfigEnabled($container, $config['serializer'])) {
if (!class_exists(\Symfony\Component\Serializer\Serializer::class)) {
throw new LogicException('Serializer support cannot be enabled as the Serializer component is not installed. Try running "composer require symfony/serializer-pack".');
diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/schema/symfony-1.0.xsd b/src/Symfony/Bundle/FrameworkBundle/Resources/config/schema/symfony-1.0.xsd
index a91698bc80684..94ec4092664da 100644
--- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/schema/symfony-1.0.xsd
+++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/schema/symfony-1.0.xsd
@@ -29,6 +29,7 @@
+
@@ -346,6 +347,18 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/web.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/web.php
index 8d1934e345ed6..362ae408b57ba 100644
--- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/web.php
+++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/web.php
@@ -102,6 +102,7 @@
param('kernel.error_controller'),
service('logger')->nullOnInvalid(),
param('kernel.debug'),
+ abstract_arg('an exceptions to log & status code mapping'),
])
->tag('kernel.event_subscriber')
->tag('monolog.logger', ['channel' => 'request'])
diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php
index d47ced3796e7a..b2a2906f0652e 100644
--- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php
+++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php
@@ -579,6 +579,7 @@ class_exists(SemaphoreStore::class) && SemaphoreStore::isSupported() ? 'semaphor
'name_based_uuid_version' => 5,
'time_based_uuid_version' => 6,
],
+ 'exceptions' => [],
];
}
}
diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/exceptions.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/exceptions.php
new file mode 100644
index 0000000000000..5d0dde0e0ac64
--- /dev/null
+++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/exceptions.php
@@ -0,0 +1,12 @@
+loadFromExtension('framework', [
+ 'exceptions' => [
+ BadRequestHttpException::class => [
+ 'log_level' => 'info',
+ 'status_code' => 422,
+ ],
+ ],
+]);
diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/exceptions.xml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/exceptions.xml
new file mode 100644
index 0000000000000..cc73b8de3ced6
--- /dev/null
+++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/exceptions.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+
+
diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/exceptions.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/exceptions.yml
new file mode 100644
index 0000000000000..82fab4e04a9f9
--- /dev/null
+++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/exceptions.yml
@@ -0,0 +1,5 @@
+framework:
+ exceptions:
+ Symfony\Component\HttpKernel\Exception\BadRequestHttpException:
+ log_level: info
+ status_code: 422
diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php
index fee9e7f8f9f0e..77860c53762cc 100644
--- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php
+++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php
@@ -521,6 +521,18 @@ public function testPhpErrorsWithLogLevels()
], $definition->getArgument(2));
}
+ public function testExceptionsConfig()
+ {
+ $container = $this->createContainerFromFile('exceptions');
+
+ $this->assertSame([
+ \Symfony\Component\HttpKernel\Exception\BadRequestHttpException::class => [
+ 'log_level' => 'info',
+ 'status_code' => 422,
+ ],
+ ], $container->getDefinition('exception_listener')->getArgument(3));
+ }
+
public function testRouter()
{
$container = $this->createContainerFromFile('full');
diff --git a/src/Symfony/Component/HttpKernel/CHANGELOG.md b/src/Symfony/Component/HttpKernel/CHANGELOG.md
index fd2bdf12154c6..7df4eb26f304c 100644
--- a/src/Symfony/Component/HttpKernel/CHANGELOG.md
+++ b/src/Symfony/Component/HttpKernel/CHANGELOG.md
@@ -6,6 +6,7 @@ CHANGELOG
* Deprecate `AbstractTestSessionListener::getSession` inject a session in the request instead
* Deprecate the `fileLinkFormat` parameter of `DebugHandlersListener`
+ * Add support for configuring log level, and status code by exception class
5.3
---
diff --git a/src/Symfony/Component/HttpKernel/EventListener/ErrorListener.php b/src/Symfony/Component/HttpKernel/EventListener/ErrorListener.php
index f5cac76bd8d0f..bc4ca2f7d861d 100644
--- a/src/Symfony/Component/HttpKernel/EventListener/ErrorListener.php
+++ b/src/Symfony/Component/HttpKernel/EventListener/ErrorListener.php
@@ -32,19 +32,30 @@ class ErrorListener implements EventSubscriberInterface
protected $controller;
protected $logger;
protected $debug;
+ protected $exceptionsMapping;
- public function __construct($controller, LoggerInterface $logger = null, bool $debug = false)
+ public function __construct($controller, LoggerInterface $logger = null, bool $debug = false, array $exceptionsMapping = [])
{
$this->controller = $controller;
$this->logger = $logger;
$this->debug = $debug;
+ $this->exceptionsMapping = $exceptionsMapping;
}
public function logKernelException(ExceptionEvent $event)
{
- $e = FlattenException::createFromThrowable($event->getThrowable());
+ $throwable = $event->getThrowable();
+ $logLevel = null;
+ foreach ($this->exceptionsMapping as $class => $config) {
+ if ($throwable instanceof $class && $config['log_level']) {
+ $logLevel = $config['log_level'];
+ break;
+ }
+ }
+
+ $e = FlattenException::createFromThrowable($throwable);
- $this->logException($event->getThrowable(), sprintf('Uncaught PHP Exception %s: "%s" at %s line %s', $e->getClass(), $e->getMessage(), $e->getFile(), $e->getLine()));
+ $this->logException($throwable, sprintf('Uncaught PHP Exception %s: "%s" at %s line %s', $e->getClass(), $e->getMessage(), $e->getFile(), $e->getLine()), $logLevel);
}
public function onKernelException(ExceptionEvent $event)
@@ -53,8 +64,8 @@ public function onKernelException(ExceptionEvent $event)
return;
}
- $exception = $event->getThrowable();
- $request = $this->duplicateRequest($exception, $event->getRequest());
+ $throwable = $event->getThrowable();
+ $request = $this->duplicateRequest($throwable, $event->getRequest());
try {
$response = $event->getKernel()->handle($request, HttpKernelInterface::SUB_REQUEST, false);
@@ -65,18 +76,25 @@ public function onKernelException(ExceptionEvent $event)
$prev = $e;
do {
- if ($exception === $wrapper = $prev) {
+ if ($throwable === $wrapper = $prev) {
throw $e;
}
} while ($prev = $wrapper->getPrevious());
$prev = new \ReflectionProperty($wrapper instanceof \Exception ? \Exception::class : \Error::class, 'previous');
$prev->setAccessible(true);
- $prev->setValue($wrapper, $exception);
+ $prev->setValue($wrapper, $throwable);
throw $e;
}
+ foreach ($this->exceptionsMapping as $exception => $config) {
+ if ($throwable instanceof $exception && $config['status_code']) {
+ $response->setStatusCode($config['status_code']);
+ break;
+ }
+ }
+
$event->setResponse($response);
if ($this->debug) {
@@ -124,10 +142,12 @@ public static function getSubscribedEvents(): array
/**
* Logs an exception.
*/
- protected function logException(\Throwable $exception, string $message): void
+ protected function logException(\Throwable $exception, string $message, string $logLevel = null): void
{
if (null !== $this->logger) {
- if (!$exception instanceof HttpExceptionInterface || $exception->getStatusCode() >= 500) {
+ if (null !== $logLevel) {
+ $this->logger->log($logLevel, $message, ['exception' => $exception]);
+ } elseif (!$exception instanceof HttpExceptionInterface || $exception->getStatusCode() >= 500) {
$this->logger->critical($message, ['exception' => $exception]);
} else {
$this->logger->error($message, ['exception' => $exception]);
diff --git a/src/Symfony/Component/HttpKernel/Tests/EventListener/ErrorListenerTest.php b/src/Symfony/Component/HttpKernel/Tests/EventListener/ErrorListenerTest.php
index bd4b1799352d7..2c8d725466e20 100644
--- a/src/Symfony/Component/HttpKernel/Tests/EventListener/ErrorListenerTest.php
+++ b/src/Symfony/Component/HttpKernel/Tests/EventListener/ErrorListenerTest.php
@@ -97,6 +97,27 @@ public function testHandleWithLogger($event, $event2)
$this->assertCount(3, $logger->getLogs('critical'));
}
+ public function testHandleWithLoggerAndCustomConfiguration()
+ {
+ $request = new Request();
+ $event = new ExceptionEvent(new TestKernel(), $request, HttpKernelInterface::MAIN_REQUEST, new \RuntimeException('bar'));
+ $logger = new TestLogger();
+ $l = new ErrorListener('not used', $logger, false, [
+ \RuntimeException::class => [
+ 'log_level' => 'warning',
+ 'status_code' => 401,
+ ],
+ ]);
+ $l->logKernelException($event);
+ $l->onKernelException($event);
+
+ $this->assertEquals(new Response('foo', 401), $event->getResponse());
+
+ $this->assertEquals(0, $logger->countErrors());
+ $this->assertCount(0, $logger->getLogs('critical'));
+ $this->assertCount(1, $logger->getLogs('warning'));
+ }
+
public function provider()
{
if (!class_exists(Request::class)) {