Skip to content

Commit 2d70525

Browse files
ArnoudThibautalanpoulainsoyuka
authored
fix(metadata): configure exceptionToStatus on an operation (#4861)
* fix: fix exception to status mapping in ApiResource and Operations Extend Symfony ErrorListener to customize ErrorListener::duplicateRequest behavior and allow to pass the '_api_operation' request attribute to the ExceptionAction in order to retrieve the exception to status mapping configured on the Resource or Operation Co-authored-by: Alan Poulain <[email protected]> Co-authored-by: soyuka <[email protected]>
1 parent cd6f0f5 commit 2d70525

File tree

16 files changed

+334
-61
lines changed

16 files changed

+334
-61
lines changed
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
Feature: Using exception_to_status config
2+
As an API developer
3+
I can customize the status code returned if the application throws an exception
4+
5+
@createSchema
6+
@!mongodb
7+
Scenario: Configure status code via the operation exceptionToStatus to map custom NotFound error to 404
8+
When I add "Content-Type" header equal to "application/ld+json"
9+
And I send a "GET" request to "/dummy_exception_to_statuses/123"
10+
Then the response status code should be 404
11+
And the response should be in JSON
12+
And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
13+
14+
@!mongodb
15+
Scenario: Configure status code via the resource exceptionToStatus to map custom NotFound error to 400
16+
When I add "Content-Type" header equal to "application/ld+json"
17+
And I send a "PUT" request to "/dummy_exception_to_statuses/123" with body:
18+
"""
19+
{
20+
"name": "black"
21+
}
22+
"""
23+
Then the response status code should be 400
24+
And the response should be in JSON
25+
And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
26+
27+
@!mongodb
28+
Scenario: Configure status code via the config file to map FilterValidationException to 400
29+
When I add "Content-Type" header equal to "application/ld+json"
30+
And I send a "GET" request to "/dummy_exception_to_statuses"
31+
Then the response status code should be 400
32+
And the response should be in JSON
33+
And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"

src/Action/ExceptionAction.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,11 @@ public function __invoke($exception, Request $request): Response
9696

9797
private function getOperationExceptionToStatus(Request $request): array
9898
{
99+
// TODO: remove legacy layer in 3.0
100+
if ($request->attributes->has('_api_exception_to_status')) {
101+
return $request->attributes->get('_api_exception_to_status');
102+
}
103+
99104
$attributes = RequestAttributesExtractor::extractAttributes($request);
100105

101106
if ([] === $attributes || null === $this->resourceMetadataFactory) {

src/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
use ApiPlatform\Symfony\Validator\Metadata\Property\Restriction\PropertySchemaRestrictionMetadataInterface;
4444
use ApiPlatform\Symfony\Validator\ValidationGroupsGeneratorInterface;
4545
use Doctrine\Common\Annotations\Annotation;
46+
use Doctrine\Persistence\ManagerRegistry;
4647
use phpDocumentor\Reflection\DocBlockFactoryInterface;
4748
use Ramsey\Uuid\Uuid;
4849
use Symfony\Component\Cache\Adapter\ArrayAdapter;
@@ -657,6 +658,11 @@ private function registerDoctrineOrmConfiguration(ContainerBuilder $container, a
657658
return;
658659
}
659660

661+
// For older versions of doctrine bridge this allows autoconfiguration for filters
662+
if (!$container->has(ManagerRegistry::class)) {
663+
$container->setAlias(ManagerRegistry::class, 'doctrine');
664+
}
665+
660666
$container->registerForAutoconfiguration(QueryItemExtensionInterface::class)
661667
->addTag('api_platform.doctrine.orm.query_extension.item');
662668
$container->registerForAutoconfiguration(DoctrineQueryCollectionExtensionInterface::class)

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,5 +175,8 @@
175175
<tag name="api_platform.uri_variables.transformer" priority="-100" />
176176
</service>
177177

178+
<service id="api_platform.error_listener" class="ApiPlatform\Symfony\EventListener\ErrorListener" parent="exception_listener">
179+
<argument key="$controller">api_platform.action.exception</argument>
180+
</service>
178181
</services>
179182
</container>

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@
103103
<argument>api_platform.action.exception</argument>
104104
<argument type="service" id="logger" on-invalid="null" />
105105
<argument>false</argument>
106-
<argument type="service" id="exception_listener" on-invalid="null" />
106+
<argument type="service" id="api_platform.error_listener" on-invalid="null" />
107107

108108
<tag name="kernel.event_listener" event="kernel.exception" method="onKernelException" priority="-96" />
109109
<tag name="monolog.logger" channel="request" />

src/Symfony/Bundle/Resources/config/symfony.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@
7070
<argument>api_platform.action.exception</argument>
7171
<argument type="service" id="logger" on-invalid="null" />
7272
<argument>false</argument>
73-
<argument type="service" id="exception_listener" on-invalid="null" />
73+
<argument type="service" id="api_platform.error_listener" on-invalid="null" />
7474

7575
<tag name="kernel.event_listener" event="kernel.exception" method="onKernelException" priority="-96" />
7676
<tag name="monolog.logger" channel="request" />
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
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+
declare(strict_types=1);
13+
14+
namespace ApiPlatform\Symfony\EventListener;
15+
16+
use ApiPlatform\Action\ExceptionAction;
17+
use Symfony\Component\HttpFoundation\Request;
18+
use Symfony\Component\HttpKernel\EventListener\ErrorListener as SymfonyErrorListener;
19+
20+
/**
21+
* This error listener extends the Symfony one in order to add
22+
* the `_api_operation` attribute when the request is duplicated.
23+
* It will later be used to retrieve the exceptionToStatus from the operation ({@see ExceptionAction}).
24+
*/
25+
final class ErrorListener extends SymfonyErrorListener
26+
{
27+
protected function duplicateRequest(\Throwable $exception, Request $request): Request
28+
{
29+
$dup = parent::duplicateRequest($exception, $request);
30+
31+
if ($request->attributes->has('_api_operation')) {
32+
$dup->attributes->set('_api_operation', $request->attributes->get('_api_operation'));
33+
}
34+
35+
// TODO: remove legacy layer in 3.0
36+
if ($request->attributes->has('_api_exception_to_status')) {
37+
$dup->attributes->set('_api_exception_to_status', $request->attributes->get('_api_exception_to_status'));
38+
}
39+
40+
return $dup;
41+
}
42+
}

src/Symfony/EventListener/ExceptionListener.php

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,13 +28,16 @@
2828
final class ExceptionListener
2929
{
3030
/**
31-
* @var ErrorListener
31+
* @phpstan-ignore-next-line legacy may not exist
32+
*
33+
* @var ErrorListener|LegacyExceptionListener
3234
*/
3335
private $exceptionListener;
3436

3537
public function __construct($controller, LoggerInterface $logger = null, $debug = false, ErrorListener $errorListener = null)
3638
{
37-
$this->exceptionListener = $errorListener ? new ErrorListener($controller, $logger, $debug) : new LegacyExceptionListener($controller, $logger, $debug); // @phpstan-ignore-line
39+
// @phpstan-ignore-next-line legacy may not exist
40+
$this->exceptionListener = $errorListener ?: new LegacyExceptionListener($controller, $logger, $debug);
3841
}
3942

4043
public function onKernelException(ExceptionEvent $event): void
@@ -48,7 +51,7 @@ public function onKernelException(ExceptionEvent $event): void
4851
return;
4952
}
5053

51-
$this->exceptionListener->onKernelException($event);
54+
$this->exceptionListener->onKernelException($event); // @phpstan-ignore-line
5255
}
5356
}
5457

src/Symfony/Routing/ApiLoader.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -336,6 +336,7 @@ private function addRoute(RouteCollection $routeCollection, string $resourceClas
336336
'_api_resource_class' => $resourceClass,
337337
'_api_identifiers' => $operation['identifiers'] ?? [],
338338
'_api_has_composite_identifier' => $operation['has_composite_identifier'] ?? true,
339+
'_api_exception_to_status' => $operation['exception_to_status'] ?? $resourceMetadata->getAttribute('exception_to_status') ?? [],
339340
'_api_operation_name' => RouteNameGenerator::generate($operationName, $resourceShortName, $operationType),
340341
sprintf('_api_%s_operation_name', $operationType) => $operationName,
341342
] + ($operation['defaults'] ?? []),

src/Test/DoctrineMongoDbOdmTestCase.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
use Doctrine\ODM\MongoDB\Mapping\Driver\AnnotationDriver;
2121
use PHPUnit\Framework\TestCase;
2222
use Symfony\Component\Cache\Adapter\ArrayAdapter;
23+
2324
use function sys_get_temp_dir;
2425

2526
/**

0 commit comments

Comments
 (0)