Skip to content

Commit e9c1863

Browse files
committed
Add the ability to configure and match exceptions with an HTTP status code
1 parent c1090d1 commit e9c1863

File tree

8 files changed

+226
-24
lines changed

8 files changed

+226
-24
lines changed

src/Action/ExceptionAction.php

+21-9
Original file line numberDiff line numberDiff line change
@@ -11,36 +11,48 @@
1111

1212
namespace ApiPlatform\Core\Action;
1313

14-
use ApiPlatform\Core\Exception\InvalidArgumentException;
1514
use ApiPlatform\Core\Util\ErrorFormatGuesser;
1615
use Symfony\Component\Debug\Exception\FlattenException;
1716
use Symfony\Component\HttpFoundation\Request;
1817
use Symfony\Component\HttpFoundation\Response;
19-
use Symfony\Component\Serializer\Exception\ExceptionInterface;
2018
use Symfony\Component\Serializer\SerializerInterface;
2119

2220
/**
2321
* Renders a normalized exception for a given {@see \Symfony\Component\Debug\Exception\FlattenException}.
2422
*
23+
* Usage:
24+
*
25+
* $exceptionAction = new ExceptionAction(
26+
* new Serializer(),
27+
* [
28+
* 'jsonproblem' => ['application/problem+json'],
29+
* 'jsonld' => ['application/ld+json'],
30+
* ],
31+
* [
32+
* ExceptionInterface::class => Response::HTTP_BAD_REQUEST,
33+
* InvalidArgumentException::class => Response::HTTP_BAD_REQUEST,
34+
* ]
35+
* );
36+
*
2537
* @author Baptiste Meyer <baptiste.meyer@gmail.com>
2638
* @author Kévin Dunglas <dunglas@gmail.com>
2739
*/
2840
final class ExceptionAction
2941
{
30-
const DEFAULT_EXCEPTION_TO_STATUS = [
31-
ExceptionInterface::class => Response::HTTP_BAD_REQUEST,
32-
InvalidArgumentException::class => Response::HTTP_BAD_REQUEST,
33-
];
34-
3542
private $serializer;
3643
private $errorFormats;
3744
private $exceptionToStatus;
3845

39-
public function __construct(SerializerInterface $serializer, array $errorFormats, $exceptionToStatus = [])
46+
/**
47+
* @param SerializerInterface $serializer
48+
* @param array $errorFormats A list of enabled formats, the first one will be the default
49+
* @param array $exceptionToStatus A list of exceptions mapped to their HTTP status code
50+
*/
51+
public function __construct(SerializerInterface $serializer, array $errorFormats, array $exceptionToStatus = [])
4052
{
4153
$this->serializer = $serializer;
4254
$this->errorFormats = $errorFormats;
43-
$this->exceptionToStatus = self::DEFAULT_EXCEPTION_TO_STATUS + $exceptionToStatus;
55+
$this->exceptionToStatus = $exceptionToStatus;
4456
}
4557

4658
/**

src/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php

+1
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ private function handleConfig(ContainerBuilder $container, array $config, array
8787
$container->setParameter('api_platform.title', $config['title']);
8888
$container->setParameter('api_platform.description', $config['description']);
8989
$container->setParameter('api_platform.version', $config['version']);
90+
$container->setParameter('api_platform.exception_to_status', $config['exception_to_status']);
9091
$container->setParameter('api_platform.formats', $formats);
9192
$container->setParameter('api_platform.error_formats', $errorFormats);
9293
$container->setParameter('api_platform.collection.order', $config['collection']['order']);

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

+60-1
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,19 @@
1111

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

14+
use ApiPlatform\Core\Exception\InvalidArgumentException;
1415
use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition;
1516
use Symfony\Component\Config\Definition\Builder\TreeBuilder;
1617
use Symfony\Component\Config\Definition\ConfigurationInterface;
18+
use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException;
19+
use Symfony\Component\HttpFoundation\Response;
20+
use Symfony\Component\Serializer\Exception\ExceptionInterface;
1721

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

44-
->arrayNode('collection')
49+
->arrayNode('collection')
4550
->addDefaultsIfNotSet()
4651
->children()
4752
->scalarNode('order')->defaultNull()->info('The default order of results.')->end()
@@ -64,6 +69,8 @@ public function getConfigTreeBuilder()
6469
->end()
6570
->end();
6671

72+
$this->addExceptionToStatusSection($rootNode);
73+
6774
$this->addFormatSection($rootNode, 'formats', [
6875
'jsonld' => ['mime_types' => ['application/ld+json']],
6976
'json' => ['mime_types' => ['application/json']], // Swagger support
@@ -77,6 +84,58 @@ public function getConfigTreeBuilder()
7784
return $treeBuilder;
7885
}
7986

87+
/**
88+
* Adds an exception to status section.
89+
*
90+
* @param ArrayNodeDefinition $rootNode
91+
*
92+
* @throws InvalidConfigurationException
93+
*/
94+
private function addExceptionToStatusSection(ArrayNodeDefinition $rootNode)
95+
{
96+
$rootNode
97+
->children()
98+
->arrayNode('exception_to_status')
99+
->defaultValue([
100+
ExceptionInterface::class => Response::HTTP_BAD_REQUEST,
101+
InvalidArgumentException::class => Response::HTTP_BAD_REQUEST,
102+
])
103+
->info('The list of exceptions mapped to their HTTP status code.')
104+
->normalizeKeys(false)
105+
->useAttributeAsKey('exception_class')
106+
->beforeNormalization()
107+
->ifArray()
108+
->then(function (array $exceptionToStatus) {
109+
foreach ($exceptionToStatus as &$httpStatusCode) {
110+
if (is_int($httpStatusCode)) {
111+
continue;
112+
}
113+
114+
if (defined($httpStatusCodeConstant = sprintf('%s::%s', Response::class, $httpStatusCode))) {
115+
$httpStatusCode = constant($httpStatusCodeConstant);
116+
}
117+
}
118+
119+
return $exceptionToStatus;
120+
})
121+
->end()
122+
->prototype('integer')->end()
123+
->validate()
124+
->ifArray()
125+
->then(function (array $exceptionToStatus) {
126+
foreach ($exceptionToStatus as $httpStatusCode) {
127+
if ($httpStatusCode < 100 || $httpStatusCode >= 600) {
128+
throw new InvalidConfigurationException(sprintf('The HTTP status code "%s" is not valid.', $httpStatusCode));
129+
}
130+
}
131+
132+
return $exceptionToStatus;
133+
})
134+
->end()
135+
->end()
136+
->end();
137+
}
138+
80139
/**
81140
* Adds a format section.
82141
*

src/Bridge/Symfony/Bundle/Resources/config/api.xml

+1
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,7 @@
168168
<service id="api_platform.action.exception" class="ApiPlatform\Core\Action\ExceptionAction">
169169
<argument type="service" id="api_platform.serializer" />
170170
<argument>%api_platform.error_formats%</argument>
171+
<argument>%api_platform.exception_to_status%</argument>
171172
</service>
172173

173174
<!-- Cache -->

tests/Action/ExceptionActionTest.php

+37-9
Original file line numberDiff line numberDiff line change
@@ -16,32 +16,60 @@
1616
use Symfony\Component\Debug\Exception\FlattenException;
1717
use Symfony\Component\HttpFoundation\Request;
1818
use Symfony\Component\HttpFoundation\Response;
19+
use Symfony\Component\Serializer\Exception\ExceptionInterface;
1920
use Symfony\Component\Serializer\SerializerInterface;
2021

2122
/**
2223
* @author Amrouche Hamza <hamza.simperfit@gmail.com>
24+
* @author Baptiste Meyer <baptiste.meyer@gmail.com>
2325
*/
2426
class ExceptionActionTest extends \PHPUnit_Framework_TestCase
2527
{
26-
public function testGetException()
28+
public function testActionWithCatchableException()
2729
{
28-
$flattenException = $this->prophesize(FlattenException::class);
29-
$flattenException->getClass()->willReturn(InvalidArgumentException::class);
30-
$flattenException->setStatusCode(Response::HTTP_BAD_REQUEST)->willReturn();
31-
$flattenException->getHeaders()->willReturn(['Content-Type' => 'application/problem+json']);
30+
$serializerException = $this->prophesize(ExceptionInterface::class);
31+
$serializerException->willExtend(\Exception::class);
32+
33+
$flattenException = FlattenException::create($serializerException->reveal());
3234

33-
$flattenException->getStatusCode()->willReturn(Response::HTTP_BAD_REQUEST);
3435
$serializer = $this->prophesize(SerializerInterface::class);
35-
$exceptionAction = new ExceptionAction($serializer->reveal(), ['jsonproblem' => ['application/problem+json'], 'jsonld' => ['application/ld+json']]);
36+
$serializer->serialize($flattenException, 'jsonproblem')->willReturn();
37+
38+
$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]);
39+
3640
$request = new Request();
3741
$request->setFormat('jsonproblem', 'application/problem+json');
38-
$serializer->serialize($flattenException, 'jsonproblem')->willReturn();
42+
3943
$expected = new Response('', Response::HTTP_BAD_REQUEST, [
4044
'Content-Type' => 'application/problem+json; charset=utf-8',
4145
'X-Content-Type-Options' => 'nosniff',
4246
'X-Frame-Options' => 'deny',
4347
]);
4448

45-
$this->assertEquals($expected, $exceptionAction($flattenException->reveal(), $request));
49+
$this->assertEquals($expected, $exceptionAction($flattenException, $request));
50+
}
51+
52+
public function testActionWithUncatchableException()
53+
{
54+
$serializerException = $this->prophesize(ExceptionInterface::class);
55+
$serializerException->willExtend(\Exception::class);
56+
57+
$flattenException = FlattenException::create($serializerException->reveal());
58+
59+
$serializer = $this->prophesize(SerializerInterface::class);
60+
$serializer->serialize($flattenException, 'jsonproblem')->willReturn();
61+
62+
$exceptionAction = new ExceptionAction($serializer->reveal(), ['jsonproblem' => ['application/problem+json'], 'jsonld' => ['application/ld+json']]);
63+
64+
$request = new Request();
65+
$request->setFormat('jsonproblem', 'application/problem+json');
66+
67+
$expected = new Response('', Response::HTTP_INTERNAL_SERVER_ERROR, [
68+
'Content-Type' => 'application/problem+json; charset=utf-8',
69+
'X-Content-Type-Options' => 'nosniff',
70+
'X-Frame-Options' => 'deny',
71+
]);
72+
73+
$this->assertEquals($expected, $exceptionAction($flattenException, $request));
4674
}
4775
}

tests/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php

+4
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
namespace ApiPlatform\Core\Tests\Bridge\Symfony\Bundle\DependencyInjection;
1313

1414
use ApiPlatform\Core\Bridge\Symfony\Bundle\DependencyInjection\ApiPlatformExtension;
15+
use ApiPlatform\Core\Exception\InvalidArgumentException;
1516
use Doctrine\Bundle\DoctrineBundle\DoctrineBundle;
1617
use FOS\UserBundle\FOSUserBundle;
1718
use Nelmio\ApiDocBundle\NelmioApiDocBundle;
@@ -23,6 +24,8 @@
2324
use Symfony\Component\DependencyInjection\Extension\ConfigurationExtensionInterface;
2425
use Symfony\Component\DependencyInjection\Extension\ExtensionInterface;
2526
use Symfony\Component\DependencyInjection\Extension\PrependExtensionInterface;
27+
use Symfony\Component\HttpFoundation\Response;
28+
use Symfony\Component\Serializer\Exception\ExceptionInterface;
2629

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

0 commit comments

Comments
 (0)