Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit 3b1cf07

Browse filesBrowse files
committed
[HttpKernel] Add support for configuring log level, and status code by exception class
1 parent 7b52f24 commit 3b1cf07
Copy full SHA for 3b1cf07

File tree

13 files changed

+171
-9
lines changed
Filter options

13 files changed

+171
-9
lines changed

‎src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md

Copy file name to clipboardExpand all lines: src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md
+1Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ CHANGELOG
88
* Deprecate the `AdapterInterface` autowiring alias, use `CacheItemPoolInterface` instead
99
* Deprecate the public `profiler` service to private
1010
* Deprecate `get()`, `has()`, `getDoctrine()`, and `dispatchMessage()` in `AbstractController`, use method/constructor injection instead
11+
* Add support for configuring log level, and status code by exception class
1112

1213
5.3
1314
---

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

Copy file name to clipboardExpand all lines: src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php
+60Lines 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);
@@ -1163,6 +1165,64 @@ private function addPhpErrorsSection(ArrayNodeDefinition $rootNode)
11631165
;
11641166
}
11651167

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

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

Copy file name to clipboardExpand all lines: src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php
+2Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -428,6 +428,8 @@ public function load(array $configs, ContainerBuilder $container)
428428
$this->registerPropertyAccessConfiguration($config['property_access'], $container, $loader);
429429
$this->registerSecretsConfiguration($config['secrets'], $container, $loader);
430430

431+
$container->getDefinition('exception_listener')->replaceArgument(3, $config['exceptions']);
432+
431433
if ($this->isConfigEnabled($container, $config['serializer'])) {
432434
if (!class_exists(\Symfony\Component\Serializer\Serializer::class)) {
433435
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

Copy file name to clipboardExpand all lines: src/Symfony/Bundle/FrameworkBundle/Resources/config/schema/symfony-1.0.xsd
+13Lines 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" />
@@ -341,6 +342,18 @@
341342
<xsd:attribute name="logLevel" type="xsd:string" />
342343
</xsd:complexType>
343344

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

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

Copy file name to clipboardExpand all lines: src/Symfony/Bundle/FrameworkBundle/Resources/config/web.php
+1Lines 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

Copy file name to clipboardExpand all lines: src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php
+1Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -578,6 +578,7 @@ class_exists(SemaphoreStore::class) && SemaphoreStore::isSupported() ? 'semaphor
578578
'name_based_uuid_version' => 5,
579579
'time_based_uuid_version' => 6,
580580
],
581+
'exceptions' => [],
581582
];
582583
}
583584
}
+12Lines 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+
]);
+13Lines 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>
+5Lines 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

Copy file name to clipboardExpand all lines: src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php
+12Lines 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/CHANGELOG.md

Copy file name to clipboardExpand all lines: src/Symfony/Component/HttpKernel/CHANGELOG.md
+1Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ CHANGELOG
66

77
* Deprecate `AbstractTestSessionListener::getSession` inject a session in the request instead
88
* Deprecate the `fileLinkFormat` parameter of `DebugHandlersListener`
9+
* Add support for configuring log level, and status code by exception class
910

1011
5.3
1112
---

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

Copy file name to clipboardExpand all lines: src/Symfony/Component/HttpKernel/EventListener/ErrorListener.php
+29-9Lines changed: 29 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -32,19 +32,30 @@ 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 $class => $config) {
50+
if ($throwable instanceof $class && $config['log_level']) {
51+
$logLevel = $config['log_level'];
52+
break;
53+
}
54+
}
55+
56+
$e = FlattenException::createFromThrowable($throwable);
4657

47-
$this->logException($event->getThrowable(), sprintf('Uncaught PHP Exception %s: "%s" at %s line %s', $e->getClass(), $e->getMessage(), $e->getFile(), $e->getLine()));
58+
$this->logException($throwable, sprintf('Uncaught PHP Exception %s: "%s" at %s line %s', $e->getClass(), $e->getMessage(), $e->getFile(), $e->getLine()), $logLevel);
4859
}
4960

5061
public function onKernelException(ExceptionEvent $event)
@@ -53,8 +64,8 @@ public function onKernelException(ExceptionEvent $event)
5364
return;
5465
}
5566

56-
$exception = $event->getThrowable();
57-
$request = $this->duplicateRequest($exception, $event->getRequest());
67+
$throwable = $event->getThrowable();
68+
$request = $this->duplicateRequest($throwable, $event->getRequest());
5869

5970
try {
6071
$response = $event->getKernel()->handle($request, HttpKernelInterface::SUB_REQUEST, false);
@@ -65,18 +76,25 @@ public function onKernelException(ExceptionEvent $event)
6576

6677
$prev = $e;
6778
do {
68-
if ($exception === $wrapper = $prev) {
79+
if ($throwable === $wrapper = $prev) {
6980
throw $e;
7081
}
7182
} while ($prev = $wrapper->getPrevious());
7283

7384
$prev = new \ReflectionProperty($wrapper instanceof \Exception ? \Exception::class : \Error::class, 'previous');
7485
$prev->setAccessible(true);
75-
$prev->setValue($wrapper, $exception);
86+
$prev->setValue($wrapper, $throwable);
7687

7788
throw $e;
7889
}
7990

91+
foreach ($this->exceptionsMapping as $exception => $config) {
92+
if ($throwable instanceof $exception && $config['status_code']) {
93+
$response->setStatusCode($config['status_code']);
94+
break;
95+
}
96+
}
97+
8098
$event->setResponse($response);
8199

82100
if ($this->debug) {
@@ -124,10 +142,12 @@ public static function getSubscribedEvents(): array
124142
/**
125143
* Logs an exception.
126144
*/
127-
protected function logException(\Throwable $exception, string $message): void
145+
protected function logException(\Throwable $exception, string $message, string $logLevel = null): void
128146
{
129147
if (null !== $this->logger) {
130-
if (!$exception instanceof HttpExceptionInterface || $exception->getStatusCode() >= 500) {
148+
if (null !== $logLevel) {
149+
$this->logger->log($logLevel, $message, ['exception' => $exception]);
150+
} elseif (!$exception instanceof HttpExceptionInterface || $exception->getStatusCode() >= 500) {
131151
$this->logger->critical($message, ['exception' => $exception]);
132152
} else {
133153
$this->logger->error($message, ['exception' => $exception]);

‎src/Symfony/Component/HttpKernel/Tests/EventListener/ErrorListenerTest.php

Copy file name to clipboardExpand all lines: src/Symfony/Component/HttpKernel/Tests/EventListener/ErrorListenerTest.php
+21Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,27 @@ public function testHandleWithLogger($event, $event2)
9797
$this->assertCount(3, $logger->getLogs('critical'));
9898
}
9999

100+
public function testHandleWithLoggerAndCustomConfiguration()
101+
{
102+
$request = new Request();
103+
$event = new ExceptionEvent(new TestKernel(), $request, HttpKernelInterface::MAIN_REQUEST, new \RuntimeException('bar'));
104+
$logger = new TestLogger();
105+
$l = new ErrorListener('not used', $logger, false, [
106+
\RuntimeException::class => [
107+
'log_level' => 'warning',
108+
'status_code' => 401,
109+
],
110+
]);
111+
$l->logKernelException($event);
112+
$l->onKernelException($event);
113+
114+
$this->assertEquals(new Response('foo', 401), $event->getResponse());
115+
116+
$this->assertEquals(0, $logger->countErrors());
117+
$this->assertCount(0, $logger->getLogs('critical'));
118+
$this->assertCount(1, $logger->getLogs('warning'));
119+
}
120+
100121
public function provider()
101122
{
102123
if (!class_exists(Request::class)) {

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.