Skip to content

Commit 8a79940

Browse files
committed
feature #2182 add a SerializerErrorRenderer (xabbuh)
This PR was merged into the 2.x branch. Discussion ---------- add a SerializerErrorRenderer Commits ------- ca200e5 add a SerializerErrorRenderer
2 parents 9e10690 + ca200e5 commit 8a79940

File tree

8 files changed

+289
-55
lines changed

8 files changed

+289
-55
lines changed

DependencyInjection/Configuration.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -525,6 +525,7 @@ private function addExceptionSection(ArrayNodeDefinition $rootNode)
525525
->defaultNull()
526526
->setDeprecated('The "%path%.%node%" option is deprecated since FOSRestBundle 2.8.')
527527
->end()
528+
->booleanNode('serializer_error_renderer')->defaultValue(false)->end()
528529
->arrayNode('codes')
529530
->useAttributeAsKey('name')
530531
->beforeNormalization()

DependencyInjection/FOSRestExtension.php

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
namespace FOS\RestBundle\DependencyInjection;
1313

14+
use FOS\RestBundle\ErrorRenderer\SerializerErrorRenderer;
1415
use FOS\RestBundle\EventListener\ResponseStatusCodeListener;
1516
use FOS\RestBundle\Inflector\DoctrineInflector;
1617
use FOS\RestBundle\View\ViewHandler;
@@ -20,6 +21,7 @@
2021
use Symfony\Component\DependencyInjection\Compiler\ServiceLocatorTagPass;
2122
use Symfony\Component\DependencyInjection\ContainerBuilder;
2223
use Symfony\Component\DependencyInjection\ContainerInterface;
24+
use Symfony\Component\DependencyInjection\Definition;
2325
use Symfony\Component\DependencyInjection\DefinitionDecorator;
2426
use Symfony\Component\DependencyInjection\Loader\XmlFileLoader;
2527
use Symfony\Component\DependencyInjection\Reference;
@@ -451,6 +453,28 @@ private function loadException(array $config, XmlFileLoader $loader, ContainerBu
451453
$container->removeDefinition('fos_rest.serializer.exception_normalizer.jms');
452454
$container->removeDefinition('fos_rest.serializer.exception_normalizer.symfony');
453455
}
456+
457+
if ($config['exception']['serializer_error_renderer']) {
458+
$format = new Definition();
459+
$format->setFactory([SerializerErrorRenderer::class, 'getPreferredFormat']);
460+
$format->setArguments([
461+
new Reference('request_stack'),
462+
]);
463+
$debug = new Definition();
464+
$debug->setFactory([SerializerErrorRenderer::class, 'isDebug']);
465+
$debug->setArguments([
466+
new Reference('request_stack'),
467+
'%kernel.debug%',
468+
]);
469+
$container->register('fos_rest.error_renderer.serializer', SerializerErrorRenderer::class)
470+
->setArguments([
471+
new Reference('fos_rest.serializer'),
472+
$format,
473+
new Reference('error_renderer.html', ContainerInterface::NULL_ON_INVALID_REFERENCE),
474+
$debug,
475+
]);
476+
$container->setAlias('error_renderer.serializer', 'fos_rest.error_renderer.serializer');
477+
}
454478
}
455479
}
456480

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the FOSRestBundle package.
5+
*
6+
* (c) FriendsOfSymfony <http://friendsofsymfony.github.com/>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace FOS\RestBundle\ErrorRenderer;
13+
14+
use FOS\RestBundle\Context\Context;
15+
use FOS\RestBundle\Serializer\Serializer;
16+
use Symfony\Component\ErrorHandler\ErrorRenderer\ErrorRendererInterface;
17+
use Symfony\Component\ErrorHandler\Exception\FlattenException;
18+
use Symfony\Component\HttpFoundation\RequestStack;
19+
use Symfony\Component\Serializer\Exception\NotEncodableValueException;
20+
21+
/**
22+
* @internal
23+
*/
24+
final class SerializerErrorRenderer implements ErrorRendererInterface
25+
{
26+
private $serializer;
27+
private $format;
28+
private $fallbackErrorRenderer;
29+
private $debug;
30+
31+
/**
32+
* @param string|callable(FlattenException) $format
33+
* @param string|bool $debug
34+
*/
35+
public function __construct(Serializer $serializer, $format, ErrorRendererInterface $fallbackErrorRenderer = null, $debug = false)
36+
{
37+
if (!is_string($format) && !is_callable($format)) {
38+
throw new \TypeError(sprintf('Argument 2 passed to "%s()" must be a string or a callable, "%s" given.', __METHOD__, \is_object($format) ? \get_class($format) : \gettype($format)));
39+
}
40+
41+
if (!is_bool($debug) && !is_callable($debug)) {
42+
throw new \TypeError(sprintf('Argument 4 passed to "%s()" must be a boolean or a callable, "%s" given.', __METHOD__, \is_object($debug) ? \get_class($debug) : \gettype($debug)));
43+
}
44+
45+
$this->serializer = $serializer;
46+
$this->format = $format;
47+
$this->fallbackErrorRenderer = $fallbackErrorRenderer;
48+
$this->debug = $debug;
49+
}
50+
51+
public function render(\Throwable $exception): FlattenException
52+
{
53+
$flattenException = FlattenException::createFromThrowable($exception);
54+
55+
try {
56+
$format = is_callable($this->format) ? ($this->format)($flattenException) : $this->format;
57+
58+
$context = new Context();
59+
$context->setAttribute('exception', $exception);
60+
$context->setAttribute('debug', is_callable($this->debug) ? ($this->debug)($exception) : $this->debug);
61+
62+
return $flattenException->setAsString($this->serializer->serialize($flattenException, $format, $context));
63+
} catch (NotEncodableValueException $e) {
64+
return $this->fallbackErrorRenderer->render($exception);
65+
}
66+
}
67+
68+
/**
69+
* @see \Symfony\Component\ErrorHandler\ErrorRenderer\SerializerErrorRenderer::getPreferredFormat
70+
*/
71+
public static function getPreferredFormat(RequestStack $requestStack): \Closure
72+
{
73+
return static function () use ($requestStack) {
74+
if (!$request = $requestStack->getCurrentRequest()) {
75+
throw new NotEncodableValueException();
76+
}
77+
78+
return $request->getPreferredFormat();
79+
};
80+
}
81+
82+
/**
83+
* @see \Symfony\Component\ErrorHandler\ErrorRenderer\HtmlErrorRenderer::isDebug
84+
*/
85+
public static function isDebug(RequestStack $requestStack, bool $debug): \Closure
86+
{
87+
return static function () use ($requestStack, $debug): bool {
88+
if (!$request = $requestStack->getCurrentRequest()) {
89+
return $debug;
90+
}
91+
92+
return $debug && $request->attributes->getBoolean('showException', true);
93+
};
94+
}
95+
}

Tests/DependencyInjection/FOSRestExtensionTest.php

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
use Symfony\Component\DependencyInjection\Definition;
2323
use Symfony\Component\DependencyInjection\DefinitionDecorator;
2424
use Symfony\Component\DependencyInjection\Reference;
25+
use Symfony\Component\ErrorHandler\ErrorRenderer\ErrorRendererInterface;
2526

2627
/**
2728
* FOSRestExtension test.
@@ -1346,4 +1347,58 @@ public function testMimeTypesArePassedArrays()
13461347
$this->container->getDefinition('fos_rest.mime_type_listener')->getArgument(0)
13471348
);
13481349
}
1350+
1351+
public function testSerializerErrorRendererNotRegisteredByDefault()
1352+
{
1353+
$config = array(
1354+
'fos_rest' => array(
1355+
'exception' => [
1356+
'exception_listener' => false,
1357+
'serialize_exceptions' => false,
1358+
],
1359+
'routing_loader' => false,
1360+
'service' => [
1361+
'templating' => null,
1362+
],
1363+
'view' => [
1364+
'default_engine' => null,
1365+
'force_redirects' => [],
1366+
],
1367+
),
1368+
);
1369+
$this->extension->load($config, $this->container);
1370+
1371+
$this->assertFalse($this->container->hasDefinition('fos_rest.error_renderer.serializer'));
1372+
$this->assertFalse($this->container->hasAlias('error_renderer.serializer'));
1373+
}
1374+
1375+
public function testRegisterSerializerErrorRenderer()
1376+
{
1377+
if (!interface_exists(ErrorRendererInterface::class)) {
1378+
$this->markTestSkipped();
1379+
}
1380+
1381+
$config = array(
1382+
'fos_rest' => array(
1383+
'exception' => [
1384+
'exception_listener' => false,
1385+
'serialize_exceptions' => false,
1386+
'serializer_error_renderer' => true,
1387+
],
1388+
'routing_loader' => false,
1389+
'service' => [
1390+
'templating' => null,
1391+
],
1392+
'view' => [
1393+
'default_engine' => null,
1394+
'force_redirects' => [],
1395+
],
1396+
),
1397+
);
1398+
$this->extension->load($config, $this->container);
1399+
1400+
$this->assertTrue($this->container->hasDefinition('fos_rest.error_renderer.serializer'));
1401+
$this->assertTrue($this->container->hasAlias('error_renderer.serializer'));
1402+
$this->assertSame('fos_rest.error_renderer.serializer', (string) $this->container->getAlias('error_renderer.serializer'));
1403+
}
13491404
}
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the FOSRestBundle package.
5+
*
6+
* (c) FriendsOfSymfony <http://friendsofsymfony.github.com/>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace FOS\RestBundle\Tests\ErrorRenderer;
13+
14+
use FOS\RestBundle\Context\Context;
15+
use FOS\RestBundle\ErrorRenderer\SerializerErrorRenderer;
16+
use FOS\RestBundle\Serializer\Serializer;
17+
use PHPUnit\Framework\TestCase;
18+
use Symfony\Component\ErrorHandler\ErrorRenderer\ErrorRendererInterface;
19+
use Symfony\Component\ErrorHandler\Exception\FlattenException;
20+
use Symfony\Component\HttpFoundation\Request;
21+
use Symfony\Component\HttpFoundation\RequestStack;
22+
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
23+
use Symfony\Component\Serializer\Exception\NotEncodableValueException;
24+
25+
class SerializerErrorRendererTest extends TestCase
26+
{
27+
protected function setUp()
28+
{
29+
if (!interface_exists(ErrorRendererInterface::class)) {
30+
$this->markTestSkipped();
31+
}
32+
}
33+
34+
public function testSerializeFlattenExceptionWithStringFormat()
35+
{
36+
$serializer = $this->createMock(Serializer::class);
37+
$serializer
38+
->expects($this->once())
39+
->method('serialize')
40+
->with($this->isInstanceOf(FlattenException::class), 'json', $this->isInstanceOf(Context::class))
41+
->willReturn('serialized FlattenException');
42+
43+
$errorRenderer = new SerializerErrorRenderer($serializer, 'json');
44+
$flattenException = $errorRenderer->render(new NotFoundHttpException());
45+
46+
$this->assertSame('serialized FlattenException', $flattenException->getAsString());
47+
}
48+
49+
public function testSerializeFlattenExceptionWithCallableFormat()
50+
{
51+
$serializer = $this->createMock(Serializer::class);
52+
$serializer
53+
->expects($this->once())
54+
->method('serialize')
55+
->with($this->isInstanceOf(FlattenException::class), 'json', $this->isInstanceOf(Context::class))
56+
->willReturn('serialized FlattenException');
57+
58+
$format = function (FlattenException $flattenException) {
59+
return 'json';
60+
};
61+
62+
$errorRenderer = new SerializerErrorRenderer($serializer, $format);
63+
$flattenException = $errorRenderer->render(new NotFoundHttpException());
64+
65+
$this->assertSame('serialized FlattenException', $flattenException->getAsString());
66+
}
67+
68+
public function testSerializeFlattenExceptionUsingGetPreferredFormatMethod()
69+
{
70+
$serializer = $this->createMock(Serializer::class);
71+
$serializer
72+
->expects($this->once())
73+
->method('serialize')
74+
->with($this->isInstanceOf(FlattenException::class), 'json', $this->isInstanceOf(Context::class))
75+
->willReturn('serialized FlattenException');
76+
77+
$request = new Request();
78+
$request->attributes->set('_format', 'json');
79+
80+
$requestStack = new RequestStack();
81+
$requestStack->push($request);
82+
$format = SerializerErrorRenderer::getPreferredFormat($requestStack);
83+
84+
$errorRenderer = new SerializerErrorRenderer($serializer, $format);
85+
$flattenException = $errorRenderer->render(new NotFoundHttpException());
86+
87+
$this->assertSame('serialized FlattenException', $flattenException->getAsString());
88+
}
89+
90+
public function testFallbackErrorRendererIsUsedWhenFormatCannotBeDetected()
91+
{
92+
$exception = new NotFoundHttpException();
93+
$flattenException = new FlattenException();
94+
95+
$fallbackErrorRenderer = $this->createMock(ErrorRendererInterface::class);
96+
$fallbackErrorRenderer
97+
->expects($this->once())
98+
->method('render')
99+
->with($exception)
100+
->willReturn($flattenException);
101+
102+
$serializer = $this->createMock(Serializer::class);
103+
$serializer->expects($this->once())
104+
->method('serialize')
105+
->with($this->isInstanceOf(FlattenException::class), 'json', $this->isInstanceOf(Context::class))
106+
->willThrowException(new NotEncodableValueException());
107+
108+
$errorRenderer = new SerializerErrorRenderer($serializer, 'json', $fallbackErrorRenderer);
109+
110+
$this->assertSame($flattenException, $errorRenderer->render($exception));
111+
}
112+
}

Tests/Functional/Bundle/TestBundle/ErrorRenderer/JmsSerializerErrorRenderer.php

Lines changed: 0 additions & 37 deletions
This file was deleted.

Tests/Functional/app/FlattenExceptionHandlerLegacyFormat/config.yml

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,12 @@ imports:
22
- { resource: ../config/default.yml }
33
- { resource: ../config/exception_listener.yml }
44

5-
services:
6-
error_renderer.serializer: '@fos_rest.error_renderer.jms_serializer_error_renderer'
7-
8-
fos_rest.error_renderer.jms_serializer_error_renderer:
9-
class: 'FOS\RestBundle\Tests\Functional\Bundle\TestBundle\ErrorRenderer\JmsSerializerErrorRenderer'
10-
arguments:
11-
- '@jms_serializer.serializer'
12-
- '@request_stack'
13-
145
fos_rest:
156
exception:
167
exception_listener: false
178
serialize_exceptions: false
189
flatten_exception_format: 'legacy'
10+
serializer_error_renderer: true
1911
routing_loader: false
2012
service:
2113
templating: ~

0 commit comments

Comments
 (0)