Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 21 additions & 9 deletions src/Action/ExceptionAction.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,36 +11,48 @@

namespace ApiPlatform\Core\Action;

use ApiPlatform\Core\Exception\InvalidArgumentException;
use ApiPlatform\Core\Util\ErrorFormatGuesser;
use Symfony\Component\Debug\Exception\FlattenException;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Serializer\Exception\ExceptionInterface;
use Symfony\Component\Serializer\SerializerInterface;

/**
* Renders a normalized exception for a given {@see \Symfony\Component\Debug\Exception\FlattenException}.
*
* Usage:
*
* $exceptionAction = new ExceptionAction(
* new Serializer(),
* [
* 'jsonproblem' => ['application/problem+json'],
* 'jsonld' => ['application/ld+json'],
* ],
* [
* ExceptionInterface::class => Response::HTTP_BAD_REQUEST,
* InvalidArgumentException::class => Response::HTTP_BAD_REQUEST,
* ]
* );
*
* @author Baptiste Meyer <baptiste.meyer@gmail.com>
* @author Kévin Dunglas <dunglas@gmail.com>
*/
final class ExceptionAction
{
const DEFAULT_EXCEPTION_TO_STATUS = [
ExceptionInterface::class => Response::HTTP_BAD_REQUEST,
InvalidArgumentException::class => Response::HTTP_BAD_REQUEST,
];

private $serializer;
private $errorFormats;
private $exceptionToStatus;

public function __construct(SerializerInterface $serializer, array $errorFormats, $exceptionToStatus = [])
/**
* @param SerializerInterface $serializer
* @param array $errorFormats A list of enabled formats, the first one will be the default
* @param array $exceptionToStatus A list of exceptions mapped to their HTTP status code
*/
public function __construct(SerializerInterface $serializer, array $errorFormats, array $exceptionToStatus = [])

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would add a phpdoc with examples of the possible values for $errorFormats and $exceptionToStatus, it is not clear from just looking at the constructor what those values can be although the tests gives some hints

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done for all @theofidry

{
$this->serializer = $serializer;
$this->errorFormats = $errorFormats;
$this->exceptionToStatus = self::DEFAULT_EXCEPTION_TO_STATUS + $exceptionToStatus;
$this->exceptionToStatus = $exceptionToStatus;
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ private function handleConfig(ContainerBuilder $container, array $config, array
$container->setParameter('api_platform.title', $config['title']);
$container->setParameter('api_platform.description', $config['description']);
$container->setParameter('api_platform.version', $config['version']);
$container->setParameter('api_platform.exception_to_status', $config['exception_to_status']);
$container->setParameter('api_platform.formats', $formats);
$container->setParameter('api_platform.error_formats', $errorFormats);
$container->setParameter('api_platform.collection.order', $config['collection']['order']);
Expand Down
61 changes: 60 additions & 1 deletion src/Bridge/Symfony/Bundle/DependencyInjection/Configuration.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,19 @@

namespace ApiPlatform\Core\Bridge\Symfony\Bundle\DependencyInjection;

use ApiPlatform\Core\Exception\InvalidArgumentException;
use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition;
use Symfony\Component\Config\Definition\Builder\TreeBuilder;
use Symfony\Component\Config\Definition\ConfigurationInterface;
use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Serializer\Exception\ExceptionInterface;

/**
* The configuration of the bundle.
*
* @author Kévin Dunglas <dunglas@gmail.com>
* @author Baptiste Meyer <baptiste.meyer@gmail.com>
*/
final class Configuration implements ConfigurationInterface
{
Expand All @@ -41,7 +46,7 @@ public function getConfigTreeBuilder()
->booleanNode('enable_nelmio_api_doc')->defaultValue(false)->info('Enable the Nelmio Api doc integration.')->end()
->booleanNode('enable_swagger')->defaultValue(true)->info('Enable the Swagger documentation and export.')->end()

->arrayNode('collection')
->arrayNode('collection')
->addDefaultsIfNotSet()
->children()
->scalarNode('order')->defaultNull()->info('The default order of results.')->end()
Expand All @@ -64,6 +69,8 @@ public function getConfigTreeBuilder()
->end()
->end();

$this->addExceptionToStatusSection($rootNode);

$this->addFormatSection($rootNode, 'formats', [
'jsonld' => ['mime_types' => ['application/ld+json']],
'json' => ['mime_types' => ['application/json']], // Swagger support
Expand All @@ -77,6 +84,58 @@ public function getConfigTreeBuilder()
return $treeBuilder;
}

/**
* Adds an exception to status section.
*
* @param ArrayNodeDefinition $rootNode
*
* @throws InvalidConfigurationException
*/
private function addExceptionToStatusSection(ArrayNodeDefinition $rootNode)
{
$rootNode
->children()
->arrayNode('exception_to_status')
->defaultValue([
ExceptionInterface::class => Response::HTTP_BAD_REQUEST,
InvalidArgumentException::class => Response::HTTP_BAD_REQUEST,
])
->info('The list of exceptions mapped to their HTTP status code.')
->normalizeKeys(false)
->useAttributeAsKey('exception_class')
->beforeNormalization()
->ifArray()
->then(function (array $exceptionToStatus) {
foreach ($exceptionToStatus as &$httpStatusCode) {
if (is_int($httpStatusCode)) {
continue;
}

if (defined($httpStatusCodeConstant = sprintf('%s::%s', Response::class, $httpStatusCode))) {
$httpStatusCode = constant($httpStatusCodeConstant);
}
}

return $exceptionToStatus;
})
->end()
->prototype('integer')->end()
->validate()
->ifArray()
->then(function (array $exceptionToStatus) {
foreach ($exceptionToStatus as $httpStatusCode) {
if ($httpStatusCode < 100 || $httpStatusCode >= 600) {
throw new InvalidConfigurationException(sprintf('The HTTP status code "%s" is not valid.', $httpStatusCode));
}
}

return $exceptionToStatus;
})
->end()
->end()
->end();
}

/**
* Adds a format section.
*
Expand Down
1 change: 1 addition & 0 deletions src/Bridge/Symfony/Bundle/Resources/config/api.xml
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,7 @@
<service id="api_platform.action.exception" class="ApiPlatform\Core\Action\ExceptionAction">
<argument type="service" id="api_platform.serializer" />
<argument>%api_platform.error_formats%</argument>
<argument>%api_platform.exception_to_status%</argument>
</service>

<!-- Cache -->
Expand Down
46 changes: 37 additions & 9 deletions tests/Action/ExceptionActionTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,32 +16,60 @@
use Symfony\Component\Debug\Exception\FlattenException;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Serializer\Exception\ExceptionInterface;
use Symfony\Component\Serializer\SerializerInterface;

/**
* @author Amrouche Hamza <hamza.simperfit@gmail.com>
* @author Baptiste Meyer <baptiste.meyer@gmail.com>
*/
class ExceptionActionTest extends \PHPUnit_Framework_TestCase
{
public function testGetException()
public function testActionWithCatchableException()
{
$flattenException = $this->prophesize(FlattenException::class);
$flattenException->getClass()->willReturn(InvalidArgumentException::class);
$flattenException->setStatusCode(Response::HTTP_BAD_REQUEST)->willReturn();
$flattenException->getHeaders()->willReturn(['Content-Type' => 'application/problem+json']);
$serializerException = $this->prophesize(ExceptionInterface::class);
$serializerException->willExtend(\Exception::class);

$flattenException = FlattenException::create($serializerException->reveal());

$flattenException->getStatusCode()->willReturn(Response::HTTP_BAD_REQUEST);
$serializer = $this->prophesize(SerializerInterface::class);
$exceptionAction = new ExceptionAction($serializer->reveal(), ['jsonproblem' => ['application/problem+json'], 'jsonld' => ['application/ld+json']]);
$serializer->serialize($flattenException, 'jsonproblem')->willReturn();

$exceptionAction = new ExceptionAction($serializer->reveal(), ['jsonproblem' => ['application/problem+json'], 'jsonld' => ['application/ld+json']], [ExceptionInterface::class => Response::HTTP_BAD_REQUEST, InvalidArgumentException::class => Response::HTTP_BAD_REQUEST]);

$request = new Request();
$request->setFormat('jsonproblem', 'application/problem+json');
$serializer->serialize($flattenException, 'jsonproblem')->willReturn();

$expected = new Response('', Response::HTTP_BAD_REQUEST, [
'Content-Type' => 'application/problem+json; charset=utf-8',
'X-Content-Type-Options' => 'nosniff',
'X-Frame-Options' => 'deny',
]);

$this->assertEquals($expected, $exceptionAction($flattenException->reveal(), $request));
$this->assertEquals($expected, $exceptionAction($flattenException, $request));
}

public function testActionWithUncatchableException()
{
$serializerException = $this->prophesize(ExceptionInterface::class);
$serializerException->willExtend(\Exception::class);

$flattenException = FlattenException::create($serializerException->reveal());

$serializer = $this->prophesize(SerializerInterface::class);
$serializer->serialize($flattenException, 'jsonproblem')->willReturn();

$exceptionAction = new ExceptionAction($serializer->reveal(), ['jsonproblem' => ['application/problem+json'], 'jsonld' => ['application/ld+json']]);

$request = new Request();
$request->setFormat('jsonproblem', 'application/problem+json');

$expected = new Response('', Response::HTTP_INTERNAL_SERVER_ERROR, [
'Content-Type' => 'application/problem+json; charset=utf-8',
'X-Content-Type-Options' => 'nosniff',
'X-Frame-Options' => 'deny',
]);

$this->assertEquals($expected, $exceptionAction($flattenException, $request));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
namespace ApiPlatform\Core\Tests\Bridge\Symfony\Bundle\DependencyInjection;

use ApiPlatform\Core\Bridge\Symfony\Bundle\DependencyInjection\ApiPlatformExtension;
use ApiPlatform\Core\Exception\InvalidArgumentException;
use Doctrine\Bundle\DoctrineBundle\DoctrineBundle;
use FOS\UserBundle\FOSUserBundle;
use Nelmio\ApiDocBundle\NelmioApiDocBundle;
Expand All @@ -23,6 +24,8 @@
use Symfony\Component\DependencyInjection\Extension\ConfigurationExtensionInterface;
use Symfony\Component\DependencyInjection\Extension\ExtensionInterface;
use Symfony\Component\DependencyInjection\Extension\PrependExtensionInterface;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Serializer\Exception\ExceptionInterface;

/**
* @author Kévin Dunglas <dunglas@gmail.com>
Expand Down Expand Up @@ -190,6 +193,7 @@ private function getContainerBuilderProphecy()
'api_platform.description' => 'description',
'api_platform.error_formats' => ['jsonproblem' => ['application/problem+json'], 'jsonld' => ['application/ld+json']],
'api_platform.formats' => ['jsonld' => ['application/ld+json'], 'jsonhal' => ['application/hal+json']],
'api_platform.exception_to_status' => [ExceptionInterface::class => Response::HTTP_BAD_REQUEST, InvalidArgumentException::class => Response::HTTP_BAD_REQUEST],
'api_platform.title' => 'title',
'api_platform.version' => 'version',
];
Expand Down
Loading