Skip to content

Commit 3d3d7b1

Browse files
committed
[HttpKernel] Add support for configuration log level, and status code by exception class
1 parent cbe1f81 commit 3d3d7b1

File tree

10 files changed

+153
-9
lines changed

10 files changed

+153
-9
lines changed

src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
use Doctrine\Common\Annotations\PsrCachedReader;
1616
use Doctrine\Common\Cache\Cache;
1717
use Doctrine\DBAL\Connection;
18+
use Psr\Log\LogLevel;
1819
use Symfony\Bundle\FullStack;
1920
use Symfony\Component\Asset\Package;
2021
use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition;
@@ -139,6 +140,7 @@ public function getConfigTreeBuilder()
139140
$this->addPropertyInfoSection($rootNode, $enableIfStandalone);
140141
$this->addCacheSection($rootNode, $willBeAvailable);
141142
$this->addPhpErrorsSection($rootNode);
143+
$this->addExceptionsSection($rootNode);
142144
$this->addWebLinkSection($rootNode, $enableIfStandalone);
143145
$this->addLockSection($rootNode, $enableIfStandalone);
144146
$this->addMessengerSection($rootNode, $enableIfStandalone);
@@ -1155,6 +1157,64 @@ private function addPhpErrorsSection(ArrayNodeDefinition $rootNode)
11551157
;
11561158
}
11571159

1160+
private function addExceptionsSection(ArrayNodeDefinition $rootNode)
1161+
{
1162+
$logLevels = (new \ReflectionClass(LogLevel::class))->getConstants();
1163+
1164+
$rootNode
1165+
->children()
1166+
->arrayNode('exceptions')
1167+
->info('Exception handling configuration')
1168+
->beforeNormalization()
1169+
->ifArray()
1170+
->then(function (array $v): array {
1171+
if (!\array_key_exists('exception', $v)) {
1172+
return $v;
1173+
};
1174+
1175+
// Fix XML normalization
1176+
$data = isset($v['exception'][0]) ? $v['exception'] : [$v['exception']];
1177+
$exceptions = [];
1178+
foreach ($data as $exception) {
1179+
$config = [];
1180+
if (array_key_exists('log-level', $exception)) {
1181+
$config['log_level'] = $exception['log-level'];
1182+
}
1183+
if (array_key_exists('status-code', $exception)) {
1184+
$config['status_code'] = $exception['status-code'];
1185+
}
1186+
$exceptions[$exception['name']] = $config;
1187+
}
1188+
1189+
return $exceptions;
1190+
})
1191+
->end()
1192+
->prototype('array')
1193+
->fixXmlConfig('exception')
1194+
->children()
1195+
->scalarNode('log_level')
1196+
->info('The level of log message. Null to let Symfony decide.')
1197+
->validate()
1198+
->ifTrue(function ($v) use ($logLevels) { return !\in_array($v, $logLevels); })
1199+
->thenInvalid(sprintf('The log level is not valid. Pick one among "%s".', implode('", "', $logLevels)))
1200+
->end()
1201+
->defaultNull()
1202+
->end()
1203+
->scalarNode('status_code')
1204+
->info('The status code of the response. Null to let Symfony decide.')
1205+
->validate()
1206+
->ifTrue(function ($v) { return !\in_array($v, range(100, 499));})
1207+
->thenInvalid('The log level is not valid. Pick one among between 100 et 599.')
1208+
->end()
1209+
->defaultNull()
1210+
->end()
1211+
->end()
1212+
->end()
1213+
->end()
1214+
->end()
1215+
;
1216+
}
1217+
11581218
private function addLockSection(ArrayNodeDefinition $rootNode, callable $enableIfStandalone)
11591219
{
11601220
$rootNode

src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -423,6 +423,8 @@ public function load(array $configs, ContainerBuilder $container)
423423
$this->registerPropertyAccessConfiguration($config['property_access'], $container, $loader);
424424
$this->registerSecretsConfiguration($config['secrets'], $container, $loader);
425425

426+
$container->getDefinition('exception_listener')->replaceArgument(3, $config['exceptions']);
427+
426428
if ($this->isConfigEnabled($container, $config['serializer'])) {
427429
if (!class_exists(\Symfony\Component\Serializer\Serializer::class)) {
428430
throw new LogicException('Serializer support cannot be enabled as the Serializer component is not installed. Try running "composer require symfony/serializer-pack".');

src/Symfony/Bundle/FrameworkBundle/Resources/config/schema/symfony-1.0.xsd

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
<xsd:element name="cache" type="cache" minOccurs="0" maxOccurs="1" />
3030
<xsd:element name="workflow" type="workflow" minOccurs="0" maxOccurs="unbounded" />
3131
<xsd:element name="php-errors" type="php-errors" minOccurs="0" maxOccurs="1" />
32+
<xsd:element name="exceptions" type="exceptions" minOccurs="0" maxOccurs="1" />
3233
<xsd:element name="lock" type="lock" minOccurs="0" maxOccurs="1" />
3334
<xsd:element name="messenger" type="messenger" minOccurs="0" maxOccurs="1" />
3435
<xsd:element name="http-client" type="http_client" minOccurs="0" maxOccurs="1" />
@@ -339,6 +340,18 @@
339340
<xsd:attribute name="logLevel" type="xsd:string" />
340341
</xsd:complexType>
341342

343+
<xsd:complexType name="exceptions">
344+
<xsd:sequence>
345+
<xsd:element name="exception" type="exception" minOccurs="0" maxOccurs="unbounded" />
346+
</xsd:sequence>
347+
</xsd:complexType>
348+
349+
<xsd:complexType name="exception">
350+
<xsd:attribute name="name" type="xsd:string" use="required" />
351+
<xsd:attribute name="log-level" type="xsd:string" />
352+
<xsd:attribute name="status-code" type="xsd:int" />
353+
</xsd:complexType>
354+
342355
<xsd:complexType name="marking_store">
343356
<xsd:sequence>
344357
<xsd:element name="argument" type="xsd:string" minOccurs="0" maxOccurs="unbounded" />

src/Symfony/Bundle/FrameworkBundle/Resources/config/web.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@
102102
param('kernel.error_controller'),
103103
service('logger')->nullOnInvalid(),
104104
param('kernel.debug'),
105+
abstract_arg('an exceptions to log & status code mapping'),
105106
])
106107
->tag('kernel.event_subscriber')
107108
->tag('monolog.logger', ['channel' => 'request'])

src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -576,6 +576,7 @@ class_exists(SemaphoreStore::class) && SemaphoreStore::isSupported() ? 'semaphor
576576
'name_based_uuid_version' => 5,
577577
'time_based_uuid_version' => 6,
578578
],
579+
'exceptions' => [],
579580
];
580581
}
581582
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<?php
2+
3+
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
4+
5+
$container->loadFromExtension('framework', [
6+
'exceptions' => [
7+
BadRequestHttpException::class => [
8+
'log_level' => 'info',
9+
'status_code' => 422,
10+
],
11+
],
12+
]);
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<?xml version="1.0" ?>
2+
<container xmlns="http://symfony.com/schema/dic/services"
3+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
4+
xmlns:framework="http://symfony.com/schema/dic/symfony"
5+
xsi:schemaLocation="http://symfony.com/schema/dic/services https://symfony.com/schema/dic/services/services-1.0.xsd
6+
http://symfony.com/schema/dic/symfony https://symfony.com/schema/dic/symfony/symfony-1.0.xsd">
7+
8+
<framework:config>
9+
<framework:exceptions>
10+
<framework:exception name="Symfony\Component\HttpKernel\Exception\BadRequestHttpException" log-level="info" status-code="422" />
11+
</framework:exceptions>
12+
</framework:config>
13+
</container>
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
framework:
2+
exceptions:
3+
Symfony\Component\HttpKernel\Exception\BadRequestHttpException:
4+
log_level: info
5+
status_code: 422

src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -522,6 +522,18 @@ public function testPhpErrorsWithLogLevels()
522522
], $definition->getArgument(2));
523523
}
524524

525+
public function testExceptionsConfig()
526+
{
527+
$container = $this->createContainerFromFile('exceptions');
528+
529+
$this->assertSame([
530+
\Symfony\Component\HttpKernel\Exception\BadRequestHttpException::class => [
531+
'log_level' => 'info',
532+
'status_code' => 422,
533+
],
534+
], $container->getDefinition('exception_listener')->getArgument(3));
535+
}
536+
525537
public function testRouter()
526538
{
527539
$container = $this->createContainerFromFile('full');

src/Symfony/Component/HttpKernel/EventListener/ErrorListener.php

Lines changed: 34 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -32,19 +32,35 @@ class ErrorListener implements EventSubscriberInterface
3232
protected $controller;
3333
protected $logger;
3434
protected $debug;
35+
protected $exceptionsMapping;
3536

36-
public function __construct($controller, LoggerInterface $logger = null, bool $debug = false)
37+
public function __construct($controller, LoggerInterface $logger = null, bool $debug = false, array $exceptionsMapping = [])
3738
{
3839
$this->controller = $controller;
3940
$this->logger = $logger;
4041
$this->debug = $debug;
42+
$this->exceptionsMapping = $exceptionsMapping;
4143
}
4244

4345
public function logKernelException(ExceptionEvent $event)
4446
{
45-
$e = FlattenException::createFromThrowable($event->getThrowable());
47+
$throwable = $event->getThrowable();
48+
$logLevel = null;
49+
foreach ($this->exceptionsMapping as $exception => $config) {
50+
if (!$throwable instanceof $exception) {
51+
continue;
52+
}
53+
54+
if (!$config['log']) {
55+
return;
56+
}
57+
$logLevel = $config['logLevel'];
58+
break;
59+
}
4660

47-
$this->logException($event->getThrowable(), sprintf('Uncaught PHP Exception %s: "%s" at %s line %s', $e->getClass(), $e->getMessage(), $e->getFile(), $e->getLine()));
61+
$e = FlattenException::createFromThrowable($throwable);
62+
63+
$this->logException($throwable, sprintf('Uncaught PHP Exception %s: "%s" at %s line %s', $e->getClass(), $e->getMessage(), $e->getFile(), $e->getLine()), $logLevel);
4864
}
4965

5066
public function onKernelException(ExceptionEvent $event)
@@ -53,8 +69,8 @@ public function onKernelException(ExceptionEvent $event)
5369
return;
5470
}
5571

56-
$exception = $event->getThrowable();
57-
$request = $this->duplicateRequest($exception, $event->getRequest());
72+
$throwable = $event->getThrowable();
73+
$request = $this->duplicateRequest($throwable, $event->getRequest());
5874

5975
try {
6076
$response = $event->getKernel()->handle($request, HttpKernelInterface::SUB_REQUEST, false);
@@ -65,18 +81,25 @@ public function onKernelException(ExceptionEvent $event)
6581

6682
$prev = $e;
6783
do {
68-
if ($exception === $wrapper = $prev) {
84+
if ($throwable === $wrapper = $prev) {
6985
throw $e;
7086
}
7187
} while ($prev = $wrapper->getPrevious());
7288

7389
$prev = new \ReflectionProperty($wrapper instanceof \Exception ? \Exception::class : \Error::class, 'previous');
7490
$prev->setAccessible(true);
75-
$prev->setValue($wrapper, $exception);
91+
$prev->setValue($wrapper, $throwable);
7692

7793
throw $e;
7894
}
7995

96+
foreach ($this->exceptionsMapping as $exception => $config) {
97+
if ($throwable instanceof $exception) {
98+
$response->setStatusCode($config['statusCode']);
99+
break;
100+
}
101+
}
102+
80103
$event->setResponse($response);
81104

82105
if ($this->debug) {
@@ -124,10 +147,12 @@ public static function getSubscribedEvents(): array
124147
/**
125148
* Logs an exception.
126149
*/
127-
protected function logException(\Throwable $exception, string $message): void
150+
protected function logException(\Throwable $exception, string $message, ?string $logLevel = null): void
128151
{
129152
if (null !== $this->logger) {
130-
if (!$exception instanceof HttpExceptionInterface || $exception->getStatusCode() >= 500) {
153+
if (null !== $logLevel) {
154+
$this->logger->log($logLevel, $message, ['exception' => $exception]);
155+
} elseif (!$exception instanceof HttpExceptionInterface || $exception->getStatusCode() >= 500) {
131156
$this->logger->critical($message, ['exception' => $exception]);
132157
} else {
133158
$this->logger->error($message, ['exception' => $exception]);

0 commit comments

Comments
 (0)