Skip to content

Commit

Permalink
Merge ce4acdb into 781dce7
Browse files Browse the repository at this point in the history
  • Loading branch information
julienfalque committed Mar 5, 2021
2 parents 781dce7 + ce4acdb commit ef0ec19
Show file tree
Hide file tree
Showing 6 changed files with 195 additions and 3 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Expand Up @@ -3,6 +3,7 @@
## 2.7.0

* **BC**: Change `api_platform.listener.request.add_format` priority from 7 to 28 to execute it before firewall (priority 8) (#3599)
* Allow defining `exception_to_status` per operation (#3519)
* Doctrine: Better exception to find which resource is linked to an exception (#3965)
* Doctrine: Allow mixed type value for date filter (notice if invalid) (#3870)
* Doctrine: Add `nulls_always_first` and `nulls_always_last` to `nulls_comparison` in order filter (#4103)
Expand Down
35 changes: 33 additions & 2 deletions src/Action/ExceptionAction.php
Expand Up @@ -13,7 +13,9 @@

namespace ApiPlatform\Core\Action;

use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface;
use ApiPlatform\Core\Util\ErrorFormatGuesser;
use ApiPlatform\Core\Util\RequestAttributesExtractor;
use Symfony\Component\Debug\Exception\FlattenException as LegacyFlattenException;
use Symfony\Component\ErrorHandler\Exception\FlattenException;
use Symfony\Component\HttpFoundation\Request;
Expand All @@ -31,16 +33,18 @@ final class ExceptionAction
private $serializer;
private $errorFormats;
private $exceptionToStatus;
private $resourceMetadataFactory;

/**
* @param array $errorFormats A list of enabled error formats
* @param array $exceptionToStatus A list of exceptions mapped to their HTTP status code
*/
public function __construct(SerializerInterface $serializer, array $errorFormats, array $exceptionToStatus = [])
public function __construct(SerializerInterface $serializer, array $errorFormats, array $exceptionToStatus = [], ?ResourceMetadataFactoryInterface $resourceMetadataFactory = null)
{
$this->serializer = $serializer;
$this->errorFormats = $errorFormats;
$this->exceptionToStatus = $exceptionToStatus;
$this->resourceMetadataFactory = $resourceMetadataFactory;
}

/**
Expand All @@ -53,7 +57,12 @@ public function __invoke($exception, Request $request): Response
$exceptionClass = $exception->getClass();
$statusCode = $exception->getStatusCode();

foreach ($this->exceptionToStatus as $class => $status) {
$exceptionToStatus = array_merge(
$this->exceptionToStatus,
$this->getOperationExceptionToStatus($request)
);

foreach ($exceptionToStatus as $class => $status) {
if (is_a($exceptionClass, $class, true)) {
$statusCode = $status;

Expand All @@ -69,4 +78,26 @@ public function __invoke($exception, Request $request): Response

return new Response($this->serializer->serialize($exception, $format['key'], ['statusCode' => $statusCode]), $statusCode, $headers);
}

private function getOperationExceptionToStatus(Request $request): array
{
$attributes = RequestAttributesExtractor::extractAttributes($request);

if ([] === $attributes || null === $this->resourceMetadataFactory) {
return [];
}

$resourceMetadata = $this->resourceMetadataFactory->create($attributes['resource_class']);
$operationExceptionToStatus = $resourceMetadata->getOperationAttribute($attributes, 'exception_to_status', [], false);
$resourceExceptionToStatus = $resourceMetadata->getAttribute('exception_to_status', []);

if (!\is_array($operationExceptionToStatus) || !\is_array($resourceExceptionToStatus)) {
throw new \LogicException('"exception_to_status" attribute should be an array.');
}

return array_merge(
$resourceExceptionToStatus,
$operationExceptionToStatus
);
}
}
5 changes: 4 additions & 1 deletion src/Annotation/ApiResource.php
Expand Up @@ -70,6 +70,7 @@
* @Attribute("swaggerContext", type="array"),
* @Attribute("urlGenerationStrategy", type="int"),
* @Attribute("validationGroups", type="mixed"),
* @Attribute("exceptionToStatus", type="array"),
* )
*/
#[\Attribute(\Attribute::TARGET_CLASS)]
Expand Down Expand Up @@ -170,6 +171,7 @@ final class ApiResource
* @param array $swaggerContext https://api-platform.com/docs/core/openapi/#using-the-openapi-and-swagger-contexts
* @param array $validationGroups https://api-platform.com/docs/core/validation/#using-validation-groups
* @param int $urlGenerationStrategy
* @param array $exceptionToStatus https://api-platform.com/docs/core/errors/#fine-grained-configuration
*
* @throws InvalidArgumentException
*/
Expand Down Expand Up @@ -219,7 +221,8 @@ public function __construct(
?array $swaggerContext = null,
?array $validationGroups = null,
?int $urlGenerationStrategy = null,
?bool $compositeIdentifier = null
?bool $compositeIdentifier = null,
array $exceptionToStatus = []
) {
if (!\is_array($description)) { // @phpstan-ignore-line Doctrine annotations support
[$publicProperties, $configurableAttributes] = self::getConfigMetadata();
Expand Down
1 change: 1 addition & 0 deletions src/Bridge/Symfony/Bundle/Resources/config/api.xml
Expand Up @@ -249,6 +249,7 @@
<argument type="service" id="api_platform.serializer" />
<argument>%api_platform.error_formats%</argument>
<argument>%api_platform.exception_to_status%</argument>
<argument type="service" id="api_platform.metadata.resource.metadata_factory" />
</service>

<!-- Identifiers -->
Expand Down
150 changes: 150 additions & 0 deletions tests/Action/ExceptionActionTest.php
Expand Up @@ -15,7 +15,10 @@

use ApiPlatform\Core\Action\ExceptionAction;
use ApiPlatform\Core\Exception\InvalidArgumentException;
use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface;
use ApiPlatform\Core\Metadata\Resource\ResourceMetadata;
use ApiPlatform\Core\Tests\ProphecyTrait;
use DomainException;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Debug\Exception\FlattenException as LegacyFlattenException;
use Symfony\Component\ErrorHandler\Exception\FlattenException;
Expand Down Expand Up @@ -57,6 +60,153 @@ public function testActionWithCatchableException()
$this->assertTrue($response->headers->contains('X-Frame-Options', 'deny'));
}

/**
* @dataProvider provideOperationExceptionToStatusCases
*/
public function testActionWithOperationExceptionToStatus(
array $globalExceptionToStatus,
?array $resourceExceptionToStatus,
?array $operationExceptionToStatus,
int $expectedStatusCode
) {
$exception = new DomainException();
$flattenException = FlattenException::create($exception);

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

$resourceMetadataFactory = $this->prophesize(ResourceMetadataFactoryInterface::class);
$resourceMetadataFactory->create('Foo')->willReturn(new ResourceMetadata(
'Foo',
null,
null,
[
'operation' => null !== $operationExceptionToStatus ? ['exception_to_status' => $operationExceptionToStatus] : [],
],
null,
null !== $resourceExceptionToStatus ? ['exception_to_status' => $resourceExceptionToStatus] : []
));

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

$request = new Request();
$request->setFormat('jsonproblem', 'application/problem+json');
$request->attributes->replace([
'_api_resource_class' => 'Foo',
'_api_item_operation_name' => 'operation',
]);

$response = $exceptionAction($flattenException, $request);

$this->assertSame('', $response->getContent());
$this->assertSame($expectedStatusCode, $response->getStatusCode());
$this->assertTrue($response->headers->contains('Content-Type', 'application/problem+json; charset=utf-8'));
$this->assertTrue($response->headers->contains('X-Content-Type-Options', 'nosniff'));
$this->assertTrue($response->headers->contains('X-Frame-Options', 'deny'));
}

public function provideOperationExceptionToStatusCases()
{
yield 'no mapping' => [
[],
null,
null,
500,
];

yield 'on global attributes' => [
[DomainException::class => 100],
null,
null,
100,
];

yield 'on global attributes with empty resource and operation attributes' => [
[DomainException::class => 100],
[],
[],
100,
];

yield 'on global attributes and resource attributes' => [
[DomainException::class => 100],
[DomainException::class => 200],
null,
200,
];

yield 'on global attributes and resource attributes with empty operation attributes' => [
[DomainException::class => 100],
[DomainException::class => 200],
[],
200,
];

yield 'on global attributes and operation attributes' => [
[DomainException::class => 100],
null,
[DomainException::class => 300],
300,
];

yield 'on global attributes and operation attributes with empty resource attributes' => [
[DomainException::class => 100],
[],
[DomainException::class => 300],
300,
];

yield 'on global, resource and operation attributes' => [
[DomainException::class => 100],
[DomainException::class => 200],
[DomainException::class => 300],
300,
];

yield 'on resource attributes' => [
[],
[DomainException::class => 200],
null,
200,
];

yield 'on resource attributes with empty operation attributes' => [
[],
[DomainException::class => 200],
[],
200,
];

yield 'on resource and operation attributes' => [
[],
[DomainException::class => 200],
[DomainException::class => 300],
300,
];

yield 'on operation attributes' => [
[],
null,
[DomainException::class => 300],
300,
];

yield 'on operation attributes with empty resource attributes' => [
[],
[],
[DomainException::class => 300],
300,
];
}

public function testActionWithUncatchableException()
{
$serializerException = $this->prophesize(ExceptionInterface::class);
Expand Down
6 changes: 6 additions & 0 deletions tests/Annotation/ApiResourceTest.php
Expand Up @@ -159,6 +159,9 @@ public function testConstructAttribute()
hydraContext: ['hydra' => 'foo'],
paginationViaCursor: ['foo'],
stateless: true,
exception_to_status: [
\DomainException::class => 400,
],
);
PHP
);
Expand Down Expand Up @@ -208,6 +211,9 @@ public function testConstructAttribute()
'pagination_via_cursor' => ['foo'],
'stateless' => true,
'composite_identifier' => null,
'exception_to_status' => [
\DomainException::class => 400,
],
], $resource->attributes);
}

Expand Down

0 comments on commit ef0ec19

Please sign in to comment.