Skip to content

Commit 2e9410a

Browse files
committed
feat(openapi): add error resources schemes
Allow linking ErrorResources to ApiResources to automatically generate openapi documentation for errors
1 parent a1dd0b5 commit 2e9410a

File tree

13 files changed

+268
-6
lines changed

13 files changed

+268
-6
lines changed

src/Metadata/Delete.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ public function __construct(
4848
?array $exceptionToStatus = null,
4949
?bool $queryParameterValidationEnabled = null,
5050
?array $links = null,
51+
?array $errors = null,
5152

5253
?string $shortName = null,
5354
?string $class = null,
@@ -128,6 +129,7 @@ public function __construct(
128129
exceptionToStatus: $exceptionToStatus,
129130
queryParameterValidationEnabled: $queryParameterValidationEnabled,
130131
links: $links,
132+
errors: $errors,
131133
shortName: $shortName,
132134
class: $class,
133135
paginationEnabled: $paginationEnabled,

src/Metadata/Error.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ public function __construct(
4848
?array $exceptionToStatus = null,
4949
?bool $queryParameterValidationEnabled = null,
5050
?array $links = null,
51+
?array $errors = null,
5152

5253
?string $shortName = null,
5354
?string $class = null,
@@ -123,6 +124,7 @@ public function __construct(
123124
exceptionToStatus: $exceptionToStatus,
124125
queryParameterValidationEnabled: $queryParameterValidationEnabled,
125126
links: $links,
127+
errors: $errors,
126128
shortName: $shortName,
127129
class: $class,
128130
paginationEnabled: $paginationEnabled,

src/Metadata/Get.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ public function __construct(
4848
?array $exceptionToStatus = null,
4949
?bool $queryParameterValidationEnabled = null,
5050
?array $links = null,
51+
?array $errors = null,
5152

5253
?string $shortName = null,
5354
?string $class = null,
@@ -127,6 +128,7 @@ public function __construct(
127128
exceptionToStatus: $exceptionToStatus,
128129
queryParameterValidationEnabled: $queryParameterValidationEnabled,
129130
links: $links,
131+
errors: $errors,
130132
shortName: $shortName,
131133
class: $class,
132134
paginationEnabled: $paginationEnabled,

src/Metadata/GetCollection.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ public function __construct(
4848
?array $exceptionToStatus = null,
4949
?bool $queryParameterValidationEnabled = null,
5050
?array $links = null,
51+
?array $errors = null,
5152

5253
?string $shortName = null,
5354
?string $class = null,
@@ -128,6 +129,7 @@ public function __construct(
128129
exceptionToStatus: $exceptionToStatus,
129130
queryParameterValidationEnabled: $queryParameterValidationEnabled,
130131
links: $links,
132+
errors: $errors,
131133
shortName: $shortName,
132134
class: $class,
133135
paginationEnabled: $paginationEnabled,

src/Metadata/HttpOperation.php

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
namespace ApiPlatform\Metadata;
1515

16+
use ApiPlatform\Metadata\Exception\ProblemExceptionInterface;
1617
use ApiPlatform\OpenApi\Attributes\Webhook;
1718
use ApiPlatform\OpenApi\Model\Operation as OpenApiOperation;
1819
use ApiPlatform\State\OptionsInterface;
@@ -72,11 +73,12 @@ class HttpOperation extends Operation
7273
* class?: string|null,
7374
* name?: string,
7475
* }|string|false|null $output {@see https://api-platform.com/docs/core/dto/#specifying-an-input-or-an-output-data-representation}
75-
* @param string|array|bool|null $mercure {@see https://api-platform.com/docs/core/mercure}
76-
* @param string|bool|null $messenger {@see https://api-platform.com/docs/core/messenger/#dispatching-a-resource-through-the-message-bus}
77-
* @param string|callable|null $provider {@see https://api-platform.com/docs/core/state-providers/#state-providers}
78-
* @param string|callable|null $processor {@see https://api-platform.com/docs/core/state-processors/#state-processors}
79-
* @param WebLink[]|null $links
76+
* @param string|array|bool|null $mercure {@see https://api-platform.com/docs/core/mercure}
77+
* @param string|bool|null $messenger {@see https://api-platform.com/docs/core/messenger/#dispatching-a-resource-through-the-message-bus}
78+
* @param string|callable|null $provider {@see https://api-platform.com/docs/core/state-providers/#state-providers}
79+
* @param string|callable|null $processor {@see https://api-platform.com/docs/core/state-processors/#state-processors}
80+
* @param WebLink[]|null $links
81+
* @param array<class-string<ProblemExceptionInterface>>|null $errors
8082
*/
8183
public function __construct(
8284
protected string $method = 'GET',
@@ -152,6 +154,7 @@ public function __construct(
152154
protected bool|OpenApiOperation|Webhook|null $openapi = null,
153155
protected ?array $exceptionToStatus = null,
154156
protected ?array $links = null,
157+
protected ?array $errors = null,
155158

156159
?string $shortName = null,
157160
?string $class = null,
@@ -614,4 +617,20 @@ public function withLinks(array $links): self
614617

615618
return $self;
616619
}
620+
621+
public function getErrors(): ?array
622+
{
623+
return $this->errors;
624+
}
625+
626+
/**
627+
* @param class-string<ProblemExceptionInterface>[] $errors
628+
*/
629+
public function withErrors(array $errors): self
630+
{
631+
$self = clone $this;
632+
$self->errors = $errors;
633+
634+
return $self;
635+
}
617636
}

src/Metadata/NotExposed.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ public function __construct(
6161

6262
?bool $queryParameterValidationEnabled = null,
6363
?array $links = null,
64+
?array $errors = null,
6465

6566
?string $shortName = null,
6667
?string $class = null,
@@ -137,6 +138,7 @@ public function __construct(
137138
exceptionToStatus: $exceptionToStatus,
138139
queryParameterValidationEnabled: $queryParameterValidationEnabled,
139140
links: $links,
141+
errors: $errors,
140142
shortName: $shortName,
141143
class: $class,
142144
paginationEnabled: $paginationEnabled,

src/Metadata/Patch.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ public function __construct(
4848
?array $exceptionToStatus = null,
4949
?bool $queryParameterValidationEnabled = null,
5050
?array $links = null,
51+
?array $errors = null,
5152

5253
?string $shortName = null,
5354
?string $class = null,
@@ -128,6 +129,7 @@ public function __construct(
128129
exceptionToStatus: $exceptionToStatus,
129130
queryParameterValidationEnabled: $queryParameterValidationEnabled,
130131
links: $links,
132+
errors: $errors,
131133
shortName: $shortName,
132134
class: $class,
133135
paginationEnabled: $paginationEnabled,

src/Metadata/Post.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ public function __construct(
4848
?array $exceptionToStatus = null,
4949
?bool $queryParameterValidationEnabled = null,
5050
?array $links = null,
51+
?array $errors = null,
5152

5253
?string $shortName = null,
5354
?string $class = null,
@@ -129,6 +130,7 @@ public function __construct(
129130
exceptionToStatus: $exceptionToStatus,
130131
queryParameterValidationEnabled: $queryParameterValidationEnabled,
131132
links: $links,
133+
errors: $errors,
132134
shortName: $shortName,
133135
class: $class,
134136
paginationEnabled: $paginationEnabled,

src/Metadata/Put.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ public function __construct(
4848
?array $exceptionToStatus = null,
4949
?bool $queryParameterValidationEnabled = null,
5050
?array $links = null,
51+
?array $errors = null,
5152

5253
?string $shortName = null,
5354
?string $class = null,
@@ -129,6 +130,7 @@ public function __construct(
129130
exceptionToStatus: $exceptionToStatus,
130131
queryParameterValidationEnabled: $queryParameterValidationEnabled,
131132
links: $links,
133+
errors: $errors,
132134
shortName: $shortName,
133135
class: $class,
134136
paginationEnabled: $paginationEnabled,

src/Metadata/Resource/Factory/MetadataCollectionFactoryTrait.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414
namespace ApiPlatform\Metadata\Resource\Factory;
1515

1616
use ApiPlatform\Metadata\ApiResource;
17+
use ApiPlatform\Metadata\Exception\InvalidArgumentException;
18+
use ApiPlatform\Metadata\Exception\ProblemExceptionInterface;
1719
use ApiPlatform\Metadata\Exception\RuntimeException;
1820
use ApiPlatform\Metadata\GraphQl\Operation as GraphQlOperation;
1921
use ApiPlatform\Metadata\HttpOperation;
@@ -88,6 +90,13 @@ private function buildResourceOperations(array $metadataCollection, string $reso
8890
foreach ($resource->getOperations() ?? new Operations() as $operation) {
8991
[$key, $operation] = $this->getOperationWithDefaults($resource, $operation);
9092
$operations[$key] = $operation;
93+
if (null !== $operation->getErrors()) {
94+
foreach ($operation->getErrors() as $error) {
95+
if (!is_a($error, ProblemExceptionInterface::class, true)) {
96+
throw new InvalidArgumentException(\sprintf('The error class "%s" does not implement "%s". Did you forget a use statement?', $error, ProblemExceptionInterface::class));
97+
}
98+
}
99+
}
91100
}
92101
if ($operations) {
93102
$resource = $resource->withOperations(new Operations($operations));

src/OpenApi/Factory/OpenApiFactory.php

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@
1919
use ApiPlatform\JsonSchema\SchemaFactoryInterface;
2020
use ApiPlatform\Metadata\ApiResource;
2121
use ApiPlatform\Metadata\CollectionOperationInterface;
22+
use ApiPlatform\Metadata\Error;
23+
use ApiPlatform\Metadata\Exception\ProblemExceptionInterface;
24+
use ApiPlatform\Metadata\Exception\ResourceClassNotFoundException;
2225
use ApiPlatform\Metadata\HeaderParameterInterface;
2326
use ApiPlatform\Metadata\HttpOperation;
2427
use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface;
@@ -48,6 +51,7 @@
4851
use ApiPlatform\OpenApi\Serializer\NormalizeOperationNameTrait;
4952
use ApiPlatform\State\Pagination\PaginationOptions;
5053
use Psr\Container\ContainerInterface;
54+
use Psr\Log\LoggerInterface;
5155
use Symfony\Component\PropertyInfo\Type;
5256
use Symfony\Component\Routing\RouteCollection;
5357
use Symfony\Component\Routing\RouterInterface;
@@ -67,7 +71,7 @@ final class OpenApiFactory implements OpenApiFactoryInterface
6771
private ?RouteCollection $routeCollection = null;
6872
private ?ContainerInterface $filterLocator = null;
6973

70-
public function __construct(private readonly ResourceNameCollectionFactoryInterface $resourceNameCollectionFactory, private readonly ResourceMetadataCollectionFactoryInterface $resourceMetadataFactory, private readonly PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, private readonly PropertyMetadataFactoryInterface $propertyMetadataFactory, private readonly SchemaFactoryInterface $jsonSchemaFactory, ContainerInterface $filterLocator, private readonly array $formats = [], ?Options $openApiOptions = null, ?PaginationOptions $paginationOptions = null, private readonly ?RouterInterface $router = null)
74+
public function __construct(private readonly ResourceNameCollectionFactoryInterface $resourceNameCollectionFactory, private readonly ResourceMetadataCollectionFactoryInterface $resourceMetadataFactory, private readonly PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, private readonly PropertyMetadataFactoryInterface $propertyMetadataFactory, private readonly SchemaFactoryInterface $jsonSchemaFactory, ContainerInterface $filterLocator, private readonly array $formats, ?Options $openApiOptions, ?PaginationOptions $paginationOptions, private readonly ?RouterInterface $router, private readonly ?LoggerInterface $logger)
7175
{
7276
$this->filterLocator = $filterLocator;
7377
$this->openApiOptions = $openApiOptions ?: new Options('API Platform');
@@ -269,6 +273,48 @@ private function collectPaths(ApiResource $resource, ResourceMetadataCollection
269273
$openapiOperation = $openapiOperation->withParameters($openapiParameters);
270274
$existingResponses = $openapiOperation->getResponses() ?: [];
271275
$overrideResponses = $operation->getExtraProperties()[self::OVERRIDE_OPENAPI_RESPONSES] ?? $this->openApiOptions->getOverrideResponses();
276+
if ($operation instanceof HttpOperation && null !== $operation->getErrors()) {
277+
foreach ($operation->getErrors() as $error) {
278+
$status = null;
279+
$description = null;
280+
try {
281+
/** @var ProblemExceptionInterface $exception */
282+
$exception = (new \ReflectionClass($error))->newInstanceWithoutConstructor();
283+
$status = $exception->getStatus();
284+
$description = $exception->getTitle();
285+
} catch (\ReflectionException) {
286+
}
287+
288+
try {
289+
$errorOperation = $this->resourceMetadataFactory->create($error)->getOperation();
290+
if (!is_a($errorOperation, Error::class)) {
291+
$this->logger?->warning(\sprintf('The error class %s is not an ErrorResource', $error));
292+
continue;
293+
}
294+
$status ??= $errorOperation->getStatus();
295+
$description ??= $errorOperation->getDescription();
296+
} catch (ResourceClassNotFoundException) {
297+
$this->logger?->warning(\sprintf('The error class %s is not an ErrorResource', $error));
298+
continue;
299+
}
300+
301+
if (!$status) {
302+
$this->logger?->error(\sprintf(
303+
'The error class %s has no status defined, please either implement ProblemExceptionInterface, or make it an ErrorResource with a status',
304+
$error));
305+
continue;
306+
}
307+
308+
$operationErrorSchemas = [];
309+
foreach ($responseMimeTypes as $operationFormat) {
310+
$operationErrorSchema = $this->jsonSchemaFactory->buildSchema($error, $operationFormat, Schema::TYPE_OUTPUT, null, $schema, null, $forceSchemaCollection);
311+
$operationErrorSchemas[$operationFormat] = $operationErrorSchema;
312+
$this->appendSchemaDefinitions($schemas, $operationErrorSchema->getDefinitions());
313+
}
314+
315+
$openapiOperation = $this->buildOpenApiResponse($existingResponses, $status, $description ?? '', $openapiOperation, $operation, $responseMimeTypes, $operationErrorSchemas, $resourceMetadataCollection);
316+
}
317+
}
272318
if ($overrideResponses || !$existingResponses) {
273319
// Create responses
274320
switch ($method) {

0 commit comments

Comments
 (0)