Skip to content

Commit a0a6b78

Browse files
committed
Refactor exception support
1 parent 7d275f0 commit a0a6b78

File tree

11 files changed

+188
-115
lines changed

11 files changed

+188
-115
lines changed

src/Action/ExceptionAction.php

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <[email protected]>
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 ApiPlatform\Core\Action;
13+
14+
use ApiPlatform\Core\Exception\InvalidArgumentException;
15+
use Symfony\Component\Debug\Exception\FlattenException;
16+
use Symfony\Component\HttpFoundation\JsonResponse;
17+
use Symfony\Component\HttpFoundation\Request;
18+
use Symfony\Component\HttpFoundation\Response;
19+
use Symfony\Component\Serializer\Exception\ExceptionInterface;
20+
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
21+
use Symfony\Component\Serializer\SerializerInterface;
22+
23+
/**
24+
* Renders a normalized exception for a given {@see \Symfony\Component\Debug\Exception\FlattenException}.
25+
*
26+
* @author Baptiste Meyer <[email protected]>
27+
* @author Kévin Dunglas <[email protected]>
28+
*/
29+
final class ExceptionAction
30+
{
31+
const DEFAULT_EXCEPTION_TO_STATUS = [
32+
ExceptionInterface::class => Response::HTTP_BAD_REQUEST,
33+
InvalidArgumentException::class => Response::HTTP_BAD_REQUEST,
34+
];
35+
36+
private $serializer;
37+
private $exceptionFormats;
38+
private $exceptionToStatus;
39+
40+
public function __construct(SerializerInterface $serializer, array $exceptionFormats, $exceptionToStatus = [])
41+
{
42+
$this->serializer = $serializer;
43+
$this->exceptionFormats = $exceptionFormats;
44+
$this->exceptionToStatus = array_merge(self::DEFAULT_EXCEPTION_TO_STATUS, $exceptionToStatus);
45+
}
46+
47+
/**
48+
* Converts a an exception to a JSON response.
49+
*
50+
* @param FlattenException $exception
51+
* @param Request $request
52+
*
53+
* @return Response
54+
*/
55+
public function __invoke(FlattenException $exception, Request $request) : Response
56+
{
57+
$exceptionClass = $exception->getClass();
58+
foreach ($this->exceptionToStatus as $class => $status) {
59+
if (is_a($exceptionClass, $class, true)) {
60+
$exception->setStatusCode($status);
61+
62+
break;
63+
}
64+
}
65+
66+
$headers = $exception->getHeaders();
67+
68+
$format = $this->getErrorFormat($request);
69+
$headers['Content-Type'] = $format['value'][0];
70+
71+
return new Response($this->serializer->serialize($exception, $format['key']), $exception->getStatusCode(), $headers);
72+
}
73+
74+
/**
75+
* Get the error format and its associated MIME type.
76+
*
77+
* @param Request $request
78+
*
79+
* @return array
80+
*/
81+
private function getErrorFormat(Request $request)
82+
{
83+
$requestFormat = $request->getRequestFormat(null);
84+
if (null === $requestFormat || !isset($this->exceptionFormats[$requestFormat])) {
85+
return ['key' => $requestFormat, 'value' => $this->exceptionFormats[$requestFormat]];
86+
}
87+
88+
reset($this->exceptionFormats);
89+
90+
return each($this->exceptionFormats);
91+
}
92+
}

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

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -53,23 +53,19 @@ public function load(array $configs, ContainerBuilder $container)
5353
$configuration = new Configuration();
5454
$config = $this->processConfiguration($configuration, $configs);
5555

56-
$formats = [];
57-
foreach ($config['formats'] as $format => $value) {
58-
foreach ($value['mime_types'] as $mimeType) {
59-
$formats[$format][] = $mimeType;
60-
}
61-
}
62-
6356
$container->setAlias('api_platform.naming.resource_path_naming_strategy', $config['naming']['resource_path_naming_strategy']);
6457

6558
if ($config['name_converter']) {
6659
$container->setAlias('api_platform.name_converter', $config['name_converter']);
6760
}
6861

62+
$formats = $this->getFormats($config['formats']);
63+
6964
$container->setParameter('api_platform.title', $config['title']);
7065
$container->setParameter('api_platform.description', $config['description']);
7166
$container->setParameter('api_platform.version', $config['version']);
7267
$container->setParameter('api_platform.formats', $formats);
68+
$container->setParameter('api_platform.error_formats', $this->getFormats($config['error_formats']));
7369
$container->setParameter('api_platform.collection.order', $config['collection']['order']);
7470
$container->setParameter('api_platform.collection.order_parameter_name', $config['collection']['order_parameter_name']);
7571
$container->setParameter('api_platform.collection.pagination.enabled', $config['collection']['pagination']['enabled']);
@@ -179,4 +175,23 @@ private function registerFileLoaders(ContainerBuilder $container)
179175
$container->getDefinition('api_platform.metadata.resource.name_collection_factory.xml')->replaceArgument(0, $xmlResources);
180176
$container->getDefinition('api_platform.metadata.resource.metadata_factory.xml')->replaceArgument(0, $xmlResources);
181177
}
178+
179+
/**
180+
* Normalizes the format from config to the one accepted by Symfony HttpFoundation.
181+
*
182+
* @param array $configFormats
183+
*
184+
* @return array
185+
*/
186+
private function getFormats(array $configFormats) : array
187+
{
188+
$formats = [];
189+
foreach ($configFormats as $format => $value) {
190+
foreach ($value['mime_types'] as $mimeType) {
191+
$formats[$format][] = $mimeType;
192+
}
193+
}
194+
195+
return $formats;
196+
}
182197
}

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

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

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

14+
use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition;
1415
use Symfony\Component\Config\Definition\Builder\TreeBuilder;
1516
use Symfony\Component\Config\Definition\ConfigurationInterface;
1617

@@ -34,31 +35,6 @@ public function getConfigTreeBuilder()
3435
->scalarNode('title')->defaultValue('')->info('The title of the API.')->end()
3536
->scalarNode('description')->defaultValue('')->info('The description of the API.')->end()
3637
->scalarNode('version')->defaultValue('0.0.0')->info('The version of the API.')->end()
37-
->arrayNode('formats')
38-
->defaultValue(['jsonld' => ['mime_types' => ['application/ld+json']]])
39-
->info('The list of enabled formats. The first one will be the default.')
40-
->normalizeKeys(false)
41-
->useAttributeAsKey('format')
42-
->beforeNormalization()
43-
->ifArray()
44-
->then(function ($v) {
45-
foreach ($v as $format => $value) {
46-
if (isset($value['mime_types'])) {
47-
continue;
48-
}
49-
50-
$v[$format] = ['mime_types' => $value];
51-
}
52-
53-
return $v;
54-
})
55-
->end()
56-
->prototype('array')
57-
->children()
58-
->arrayNode('mime_types')->prototype('scalar')->end()->end()
59-
->end()
60-
->end()
61-
->end()
6238
->arrayNode('naming')
6339
->addDefaultsIfNotSet()
6440
->children()
@@ -92,6 +68,48 @@ public function getConfigTreeBuilder()
9268
->end()
9369
->end();
9470

71+
$this->addFormatSection($rootNode, 'formats', ['jsonld' => ['mime_types' => ['application/ld+json']]]);
72+
$this->addFormatSection($rootNode, 'error_formats', ['jsonld' => ['mime_types' => ['application/ld+json']]]);
73+
9574
return $treeBuilder;
9675
}
76+
77+
/**
78+
* Adds a format section.
79+
*
80+
* @param ArrayNodeDefinition $rootNode
81+
* @param string $key
82+
* @param array $defaultValue
83+
*/
84+
private function addFormatSection(ArrayNodeDefinition $rootNode, string $key, array $defaultValue)
85+
{
86+
$rootNode
87+
->children()
88+
->arrayNode($key)
89+
->defaultValue($defaultValue)
90+
->info('The list of enabled formats. The first one will be the default.')
91+
->normalizeKeys(false)
92+
->useAttributeAsKey('format')
93+
->beforeNormalization()
94+
->ifArray()
95+
->then(function ($v) {
96+
foreach ($v as $format => $value) {
97+
if (isset($value['mime_types'])) {
98+
continue;
99+
}
100+
101+
$v[$format] = ['mime_types' => $value];
102+
}
103+
104+
return $v;
105+
})
106+
->end()
107+
->prototype('array')
108+
->children()
109+
->arrayNode('mime_types')->prototype('scalar')->end()->end()
110+
->end()
111+
->end()
112+
->end()
113+
->end();
114+
}
97115
}

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

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,14 @@
108108
<tag name="kernel.event_listener" event="kernel.view" method="onKernelView" priority="8" />
109109
</service>
110110

111+
<service id="api_platform.listener.exception" class="ApiPlatform\Core\EventListener\ExceptionListener">
112+
<argument>api_platform.action.exception</argument>
113+
<argument type="service" id="logger" on-invalid="null" />
114+
115+
<tag name="kernel.event_listener" event="kernel.exception" method="onKernelException" priority="-96" />
116+
<tag name="monolog.logger" channel="request" />
117+
</service>
118+
111119
<!-- Action -->
112120

113121
<service id="api_platform.action.placeholder" class="ApiPlatform\Core\Action\PlaceholderAction" />
@@ -120,6 +128,11 @@
120128
<service id="api_platform.action.entrypoint" class="ApiPlatform\Core\Action\EntrypointAction">
121129
<argument type="service" id="api_platform.metadata.resource.name_collection_factory" />
122130
</service>
131+
132+
<service id="api_platform.action.exception" class="ApiPlatform\Core\Action\ExceptionAction">
133+
<argument type="service" id="api_platform.serializer" />
134+
<argument>%api_platform.error_formats%</argument>
135+
</service>
123136
</services>
124137

125138
</container>

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

Lines changed: 7 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -31,14 +31,6 @@
3131
<tag name="kernel.event_listener" event="kernel.exception" method="onKernelException" />
3232
</service>
3333

34-
<service id="api_platform.hydra.listener.exception" class="ApiPlatform\Core\Hydra\EventListener\ExceptionListener">
35-
<argument>api_platform.hydra.action.exception</argument>
36-
<argument type="service" id="logger" on-invalid="null" />
37-
38-
<tag name="kernel.event_listener" event="kernel.exception" method="onKernelException" priority="-96" />
39-
<tag name="monolog.logger" channel="request" />
40-
</service>
41-
4234
<!-- Serializer -->
4335

4436
<service id="api_platform.hydra.normalizer.resource_name_collection" class="ApiPlatform\Core\Hydra\Serializer\ResourceNameCollectionNormalizer" public="false">
@@ -49,6 +41,13 @@
4941
<tag name="serializer.normalizer" priority="32" />
5042
</service>
5143

44+
<service id="api_platform.hydra.normalizer.error" class="ApiPlatform\Core\Hydra\Serializer\ErrorNormalizer" public="false">
45+
<argument type="service" id="api_platform.router" />
46+
<argument>%kernel.debug%</argument>
47+
48+
<tag name="serializer.normalizer" priority="32" />
49+
</service>
50+
5251
<service id="api_platform.hydra.normalizer.collection" class="ApiPlatform\Core\Hydra\Serializer\CollectionNormalizer" public="false">
5352
<argument type="service" id="api_platform.jsonld.context_builder" />
5453
<argument type="service" id="api_platform.resource_class_resolver" />
@@ -63,13 +62,6 @@
6362
<tag name="serializer.normalizer" />
6463
</service>
6564

66-
<service id="api_platform.hydra.normalizer.error" class="ApiPlatform\Core\Hydra\Serializer\ErrorNormalizer" public="false">
67-
<argument type="service" id="api_platform.router" />
68-
<argument>%kernel.debug%</argument>
69-
70-
<tag name="serializer.normalizer" />
71-
</service>
72-
7365
<service id="api_platform.hydra.normalizer.partial_collection_view" class="ApiPlatform\Core\Hydra\Serializer\PartialCollectionViewNormalizer" decorates="api_platform.hydra.normalizer.collection" public="false">
7466
<argument type="service" id="api_platform.hydra.normalizer.partial_collection_view.inner" />
7567
<argument>%api_platform.collection.pagination.page_parameter_name%</argument>
@@ -88,11 +80,6 @@
8880
<service id="api_platform.hydra.action.documentation" class="ApiPlatform\Core\Documentation\Action\DocumentationAction">
8981
<argument type="service" id="api_platform.hydra.documentation_builder" />
9082
</service>
91-
92-
<service id="api_platform.hydra.action.exception" class="ApiPlatform\Core\Hydra\Action\ExceptionAction">
93-
<argument type="service" id="api_platform.hydra.normalizer.error" />
94-
</service>
95-
9683
</services>
9784

9885
</container>

src/Hydra/EventListener/ExceptionListener.php renamed to src/EventListener/ExceptionListener.php

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
* file that was distributed with this source code.
1010
*/
1111

12-
namespace ApiPlatform\Core\Hydra\EventListener;
12+
namespace ApiPlatform\Core\EventListener;
1313

1414
use Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent;
1515
use Symfony\Component\HttpKernel\EventListener\ExceptionListener as BaseExceptionListener;
@@ -24,8 +24,9 @@ final class ExceptionListener extends BaseExceptionListener
2424
{
2525
public function onKernelException(GetResponseForExceptionEvent $event)
2626
{
27-
// Normalize exceptions with hydra errors only for resources
28-
if (!$event->getRequest()->attributes->has('_api_resource_class')) {
27+
$request = $event->getRequest();
28+
// Normalize exceptions only for routes managed by API Platform
29+
if (!$request->attributes->has('_api_resource_class') && !$request->attributes->has('_api_respond')) {
2930
return;
3031
}
3132

src/Hydra/Action/ExceptionAction.php

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

0 commit comments

Comments
 (0)