Skip to content

Commit

Permalink
feature #2182 add a SerializerErrorRenderer (xabbuh)
Browse files Browse the repository at this point in the history
This PR was merged into the 2.x branch.

Discussion
----------

add a SerializerErrorRenderer

Commits
-------

ca200e5 add a SerializerErrorRenderer
  • Loading branch information
xabbuh committed May 7, 2020
2 parents 9e10690 + ca200e5 commit 8a79940
Show file tree
Hide file tree
Showing 8 changed files with 289 additions and 55 deletions.
1 change: 1 addition & 0 deletions DependencyInjection/Configuration.php
Original file line number Diff line number Diff line change
Expand Up @@ -525,6 +525,7 @@ private function addExceptionSection(ArrayNodeDefinition $rootNode)
->defaultNull()
->setDeprecated('The "%path%.%node%" option is deprecated since FOSRestBundle 2.8.')
->end()
->booleanNode('serializer_error_renderer')->defaultValue(false)->end()
->arrayNode('codes')
->useAttributeAsKey('name')
->beforeNormalization()
Expand Down
24 changes: 24 additions & 0 deletions DependencyInjection/FOSRestExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

namespace FOS\RestBundle\DependencyInjection;

use FOS\RestBundle\ErrorRenderer\SerializerErrorRenderer;
use FOS\RestBundle\EventListener\ResponseStatusCodeListener;
use FOS\RestBundle\Inflector\DoctrineInflector;
use FOS\RestBundle\View\ViewHandler;
Expand All @@ -20,6 +21,7 @@
use Symfony\Component\DependencyInjection\Compiler\ServiceLocatorTagPass;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\DefinitionDecorator;
use Symfony\Component\DependencyInjection\Loader\XmlFileLoader;
use Symfony\Component\DependencyInjection\Reference;
Expand Down Expand Up @@ -451,6 +453,28 @@ private function loadException(array $config, XmlFileLoader $loader, ContainerBu
$container->removeDefinition('fos_rest.serializer.exception_normalizer.jms');
$container->removeDefinition('fos_rest.serializer.exception_normalizer.symfony');
}

if ($config['exception']['serializer_error_renderer']) {
$format = new Definition();
$format->setFactory([SerializerErrorRenderer::class, 'getPreferredFormat']);
$format->setArguments([
new Reference('request_stack'),
]);
$debug = new Definition();
$debug->setFactory([SerializerErrorRenderer::class, 'isDebug']);
$debug->setArguments([
new Reference('request_stack'),
'%kernel.debug%',
]);
$container->register('fos_rest.error_renderer.serializer', SerializerErrorRenderer::class)
->setArguments([
new Reference('fos_rest.serializer'),
$format,
new Reference('error_renderer.html', ContainerInterface::NULL_ON_INVALID_REFERENCE),
$debug,
]);
$container->setAlias('error_renderer.serializer', 'fos_rest.error_renderer.serializer');
}
}
}

Expand Down
95 changes: 95 additions & 0 deletions ErrorRenderer/SerializerErrorRenderer.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
<?php

/*
* This file is part of the FOSRestBundle package.
*
* (c) FriendsOfSymfony <http://friendsofsymfony.github.com/>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace FOS\RestBundle\ErrorRenderer;

use FOS\RestBundle\Context\Context;
use FOS\RestBundle\Serializer\Serializer;
use Symfony\Component\ErrorHandler\ErrorRenderer\ErrorRendererInterface;
use Symfony\Component\ErrorHandler\Exception\FlattenException;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\Serializer\Exception\NotEncodableValueException;

/**
* @internal
*/
final class SerializerErrorRenderer implements ErrorRendererInterface
{
private $serializer;
private $format;
private $fallbackErrorRenderer;
private $debug;

/**
* @param string|callable(FlattenException) $format
* @param string|bool $debug
*/
public function __construct(Serializer $serializer, $format, ErrorRendererInterface $fallbackErrorRenderer = null, $debug = false)
{
if (!is_string($format) && !is_callable($format)) {
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)));
}

if (!is_bool($debug) && !is_callable($debug)) {
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)));
}

$this->serializer = $serializer;
$this->format = $format;
$this->fallbackErrorRenderer = $fallbackErrorRenderer;
$this->debug = $debug;
}

public function render(\Throwable $exception): FlattenException
{
$flattenException = FlattenException::createFromThrowable($exception);

try {
$format = is_callable($this->format) ? ($this->format)($flattenException) : $this->format;

$context = new Context();
$context->setAttribute('exception', $exception);
$context->setAttribute('debug', is_callable($this->debug) ? ($this->debug)($exception) : $this->debug);

return $flattenException->setAsString($this->serializer->serialize($flattenException, $format, $context));
} catch (NotEncodableValueException $e) {
return $this->fallbackErrorRenderer->render($exception);
}
}

/**
* @see \Symfony\Component\ErrorHandler\ErrorRenderer\SerializerErrorRenderer::getPreferredFormat
*/
public static function getPreferredFormat(RequestStack $requestStack): \Closure
{
return static function () use ($requestStack) {
if (!$request = $requestStack->getCurrentRequest()) {
throw new NotEncodableValueException();
}

return $request->getPreferredFormat();
};
}

/**
* @see \Symfony\Component\ErrorHandler\ErrorRenderer\HtmlErrorRenderer::isDebug
*/
public static function isDebug(RequestStack $requestStack, bool $debug): \Closure
{
return static function () use ($requestStack, $debug): bool {
if (!$request = $requestStack->getCurrentRequest()) {
return $debug;
}

return $debug && $request->attributes->getBoolean('showException', true);
};
}
}
55 changes: 55 additions & 0 deletions Tests/DependencyInjection/FOSRestExtensionTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\DefinitionDecorator;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\ErrorHandler\ErrorRenderer\ErrorRendererInterface;

/**
* FOSRestExtension test.
Expand Down Expand Up @@ -1346,4 +1347,58 @@ public function testMimeTypesArePassedArrays()
$this->container->getDefinition('fos_rest.mime_type_listener')->getArgument(0)
);
}

public function testSerializerErrorRendererNotRegisteredByDefault()
{
$config = array(
'fos_rest' => array(
'exception' => [
'exception_listener' => false,
'serialize_exceptions' => false,
],
'routing_loader' => false,
'service' => [
'templating' => null,
],
'view' => [
'default_engine' => null,
'force_redirects' => [],
],
),
);
$this->extension->load($config, $this->container);

$this->assertFalse($this->container->hasDefinition('fos_rest.error_renderer.serializer'));
$this->assertFalse($this->container->hasAlias('error_renderer.serializer'));
}

public function testRegisterSerializerErrorRenderer()
{
if (!interface_exists(ErrorRendererInterface::class)) {
$this->markTestSkipped();
}

$config = array(
'fos_rest' => array(
'exception' => [
'exception_listener' => false,
'serialize_exceptions' => false,
'serializer_error_renderer' => true,
],
'routing_loader' => false,
'service' => [
'templating' => null,
],
'view' => [
'default_engine' => null,
'force_redirects' => [],
],
),
);
$this->extension->load($config, $this->container);

$this->assertTrue($this->container->hasDefinition('fos_rest.error_renderer.serializer'));
$this->assertTrue($this->container->hasAlias('error_renderer.serializer'));
$this->assertSame('fos_rest.error_renderer.serializer', (string) $this->container->getAlias('error_renderer.serializer'));
}
}
112 changes: 112 additions & 0 deletions Tests/ErrorRenderer/SerializerErrorRendererTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
<?php

/*
* This file is part of the FOSRestBundle package.
*
* (c) FriendsOfSymfony <http://friendsofsymfony.github.com/>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace FOS\RestBundle\Tests\ErrorRenderer;

use FOS\RestBundle\Context\Context;
use FOS\RestBundle\ErrorRenderer\SerializerErrorRenderer;
use FOS\RestBundle\Serializer\Serializer;
use PHPUnit\Framework\TestCase;
use Symfony\Component\ErrorHandler\ErrorRenderer\ErrorRendererInterface;
use Symfony\Component\ErrorHandler\Exception\FlattenException;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\Serializer\Exception\NotEncodableValueException;

class SerializerErrorRendererTest extends TestCase
{
protected function setUp()
{
if (!interface_exists(ErrorRendererInterface::class)) {
$this->markTestSkipped();
}
}

public function testSerializeFlattenExceptionWithStringFormat()
{
$serializer = $this->createMock(Serializer::class);
$serializer
->expects($this->once())
->method('serialize')
->with($this->isInstanceOf(FlattenException::class), 'json', $this->isInstanceOf(Context::class))
->willReturn('serialized FlattenException');

$errorRenderer = new SerializerErrorRenderer($serializer, 'json');
$flattenException = $errorRenderer->render(new NotFoundHttpException());

$this->assertSame('serialized FlattenException', $flattenException->getAsString());
}

public function testSerializeFlattenExceptionWithCallableFormat()
{
$serializer = $this->createMock(Serializer::class);
$serializer
->expects($this->once())
->method('serialize')
->with($this->isInstanceOf(FlattenException::class), 'json', $this->isInstanceOf(Context::class))
->willReturn('serialized FlattenException');

$format = function (FlattenException $flattenException) {
return 'json';
};

$errorRenderer = new SerializerErrorRenderer($serializer, $format);
$flattenException = $errorRenderer->render(new NotFoundHttpException());

$this->assertSame('serialized FlattenException', $flattenException->getAsString());
}

public function testSerializeFlattenExceptionUsingGetPreferredFormatMethod()
{
$serializer = $this->createMock(Serializer::class);
$serializer
->expects($this->once())
->method('serialize')
->with($this->isInstanceOf(FlattenException::class), 'json', $this->isInstanceOf(Context::class))
->willReturn('serialized FlattenException');

$request = new Request();
$request->attributes->set('_format', 'json');

$requestStack = new RequestStack();
$requestStack->push($request);
$format = SerializerErrorRenderer::getPreferredFormat($requestStack);

$errorRenderer = new SerializerErrorRenderer($serializer, $format);
$flattenException = $errorRenderer->render(new NotFoundHttpException());

$this->assertSame('serialized FlattenException', $flattenException->getAsString());
}

public function testFallbackErrorRendererIsUsedWhenFormatCannotBeDetected()
{
$exception = new NotFoundHttpException();
$flattenException = new FlattenException();

$fallbackErrorRenderer = $this->createMock(ErrorRendererInterface::class);
$fallbackErrorRenderer
->expects($this->once())
->method('render')
->with($exception)
->willReturn($flattenException);

$serializer = $this->createMock(Serializer::class);
$serializer->expects($this->once())
->method('serialize')
->with($this->isInstanceOf(FlattenException::class), 'json', $this->isInstanceOf(Context::class))
->willThrowException(new NotEncodableValueException());

$errorRenderer = new SerializerErrorRenderer($serializer, 'json', $fallbackErrorRenderer);

$this->assertSame($flattenException, $errorRenderer->render($exception));
}
}

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,12 @@ imports:
- { resource: ../config/default.yml }
- { resource: ../config/exception_listener.yml }

services:
error_renderer.serializer: '@fos_rest.error_renderer.jms_serializer_error_renderer'

fos_rest.error_renderer.jms_serializer_error_renderer:
class: 'FOS\RestBundle\Tests\Functional\Bundle\TestBundle\ErrorRenderer\JmsSerializerErrorRenderer'
arguments:
- '@jms_serializer.serializer'
- '@request_stack'

fos_rest:
exception:
exception_listener: false
serialize_exceptions: false
flatten_exception_format: 'legacy'
serializer_error_renderer: true
routing_loader: false
service:
templating: ~
Expand Down
Loading

0 comments on commit 8a79940

Please sign in to comment.