20
20
use ApiPlatform \JsonSchema \TypeFactoryInterface ;
21
21
use ApiPlatform \Metadata \ApiResource ;
22
22
use ApiPlatform \Metadata \CollectionOperationInterface ;
23
+ use ApiPlatform \Metadata \Error ;
24
+ use ApiPlatform \Metadata \Exception \OperationNotFoundException ;
25
+ use ApiPlatform \Metadata \Exception \ProblemExceptionInterface ;
26
+ use ApiPlatform \Metadata \Exception \ResourceClassNotFoundException ;
27
+ use ApiPlatform \Metadata \Exception \RuntimeException ;
23
28
use ApiPlatform \Metadata \HeaderParameterInterface ;
24
29
use ApiPlatform \Metadata \HttpOperation ;
25
30
use ApiPlatform \Metadata \Property \Factory \PropertyMetadataFactoryInterface ;
38
43
use ApiPlatform \OpenApi \Model \MediaType ;
39
44
use ApiPlatform \OpenApi \Model \OAuthFlow ;
40
45
use ApiPlatform \OpenApi \Model \OAuthFlows ;
46
+ use ApiPlatform \OpenApi \Model \Operation ;
41
47
use ApiPlatform \OpenApi \Model \Parameter ;
42
48
use ApiPlatform \OpenApi \Model \PathItem ;
43
49
use ApiPlatform \OpenApi \Model \Paths ;
@@ -75,8 +81,19 @@ final class OpenApiFactory implements OpenApiFactoryInterface
75
81
*/
76
82
public const OPENAPI_DEFINITION_NAME = 'openapi_definition_name ' ;
77
83
78
- public function __construct (private readonly ResourceNameCollectionFactoryInterface $ resourceNameCollectionFactory , private readonly ResourceMetadataCollectionFactoryInterface $ resourceMetadataFactory , private readonly PropertyNameCollectionFactoryInterface $ propertyNameCollectionFactory , private readonly PropertyMetadataFactoryInterface $ propertyMetadataFactory , private readonly SchemaFactoryInterface $ jsonSchemaFactory , ?TypeFactoryInterface $ jsonSchemaTypeFactory , ContainerInterface $ filterLocator , private readonly array $ formats = [], ?Options $ openApiOptions = null , ?PaginationOptions $ paginationOptions = null , private readonly ?RouterInterface $ router = null )
79
- {
84
+ public function __construct (
85
+ private readonly ResourceNameCollectionFactoryInterface $ resourceNameCollectionFactory ,
86
+ private readonly ResourceMetadataCollectionFactoryInterface $ resourceMetadataFactory ,
87
+ private readonly PropertyNameCollectionFactoryInterface $ propertyNameCollectionFactory ,
88
+ private readonly PropertyMetadataFactoryInterface $ propertyMetadataFactory ,
89
+ private readonly SchemaFactoryInterface $ jsonSchemaFactory ,
90
+ ?TypeFactoryInterface $ jsonSchemaTypeFactory ,
91
+ ContainerInterface $ filterLocator ,
92
+ private readonly array $ formats = [],
93
+ ?Options $ openApiOptions = null ,
94
+ ?PaginationOptions $ paginationOptions = null ,
95
+ private readonly ?RouterInterface $ router = null ,
96
+ ) {
80
97
$ this ->filterLocator = $ filterLocator ;
81
98
$ this ->openApiOptions = $ openApiOptions ?: new Options ('API Platform ' );
82
99
$ this ->paginationOptions = $ paginationOptions ?: new PaginationOptions ();
@@ -181,15 +198,15 @@ private function collectPaths(ApiResource $resource, ResourceMetadataCollection
181
198
182
199
if ($ openapiAttribute instanceof Webhook) {
183
200
$ pathItem = $ openapiAttribute ->getPathItem () ?: new PathItem ();
184
- $ openapiOperation = $ pathItem ->{'get ' .ucfirst (strtolower ($ method ))}() ?: new Model \ Operation ();
201
+ $ openapiOperation = $ pathItem ->{'get ' .ucfirst (strtolower ($ method ))}() ?: new Operation ();
185
202
} elseif (!\is_object ($ openapiAttribute )) {
186
- $ openapiOperation = new Model \ Operation ();
203
+ $ openapiOperation = new Operation ();
187
204
} else {
188
205
$ openapiOperation = $ openapiAttribute ;
189
206
}
190
207
191
208
// Complete with defaults
192
- $ openapiOperation = new Model \ Operation (
209
+ $ openapiOperation = new Operation (
193
210
operationId: null !== $ openapiOperation ->getOperationId () ? $ openapiOperation ->getOperationId () : $ this ->normalizeOperationName ($ operationName ),
194
211
tags: null !== $ openapiOperation ->getTags () ? $ openapiOperation ->getTags () : [$ operation ->getShortName () ?: $ resourceShortName ],
195
212
responses: null !== $ openapiOperation ->getResponses () ? $ openapiOperation ->getResponses () : [],
@@ -339,6 +356,10 @@ private function collectPaths(ApiResource $resource, ResourceMetadataCollection
339
356
340
357
$ existingResponses = $ openapiOperation ?->getResponses() ?: [];
341
358
$ overrideResponses = $ operation ->getExtraProperties ()[self ::OVERRIDE_OPENAPI_RESPONSES ] ?? $ this ->openApiOptions ->getOverrideResponses ();
359
+ if ($ operation instanceof HttpOperation && null !== ($ errors = $ operation ->getErrors ())) {
360
+ $ openapiOperation = $ this ->addOperationErrors ($ openapiOperation , $ errors , $ responseMimeTypes , $ resourceMetadataCollection , $ schema , $ schemas );
361
+ }
362
+
342
363
if ($ overrideResponses || !$ existingResponses ) {
343
364
// Create responses
344
365
switch ($ method ) {
@@ -433,7 +454,7 @@ private function collectPaths(ApiResource $resource, ResourceMetadataCollection
433
454
'3.1 ' ,
434
455
'The "openapiContext" option is deprecated, use "openapi" instead. '
435
456
);
436
- $ allowedProperties = array_map (fn (\ReflectionProperty $ reflProperty ): string => $ reflProperty ->getName (), (new \ReflectionClass (Model \ Operation::class))->getProperties ());
457
+ $ allowedProperties = array_map (fn (\ReflectionProperty $ reflProperty ): string => $ reflProperty ->getName (), (new \ReflectionClass (Operation::class))->getProperties ());
437
458
foreach ($ operation ->getOpenapiContext () as $ key => $ value ) {
438
459
$ value = match ($ key ) {
439
460
'externalDocs ' => new ExternalDocumentation (description: $ value ['description ' ] ?? '' , url: $ value ['url ' ] ?? '' ),
@@ -460,7 +481,7 @@ private function collectPaths(ApiResource $resource, ResourceMetadataCollection
460
481
}
461
482
}
462
483
463
- private function buildOpenApiResponse (array $ existingResponses , int |string $ status , string $ description , ?Model \ Operation $ openapiOperation = null , ?HttpOperation $ operation = null , ?array $ responseMimeTypes = null , ?array $ operationOutputSchemas = null , ?ResourceMetadataCollection $ resourceMetadataCollection = null ): Model \ Operation
484
+ private function buildOpenApiResponse (array $ existingResponses , int |string $ status , string $ description , ?Operation $ openapiOperation = null , ?HttpOperation $ operation = null , ?array $ responseMimeTypes = null , ?array $ operationOutputSchemas = null , ?ResourceMetadataCollection $ resourceMetadataCollection = null ): Operation
464
485
{
465
486
if (isset ($ existingResponses [$ status ])) {
466
487
return $ openapiOperation ;
@@ -491,6 +512,9 @@ private function buildContent(array $responseMimeTypes, array $operationSchemas)
491
512
return $ content ;
492
513
}
493
514
515
+ /**
516
+ * @return array[array<string, string>, array<string, string>]
517
+ */
494
518
private function getMimeTypes (HttpOperation $ operation ): array
495
519
{
496
520
$ requestFormats = $ operation ->getInputFormats () ?: [];
@@ -502,6 +526,11 @@ private function getMimeTypes(HttpOperation $operation): array
502
526
return [$ requestMimeTypes , $ responseMimeTypes ];
503
527
}
504
528
529
+ /**
530
+ * @param array<string, string[]> $responseFormats
531
+ *
532
+ * @return array<string, string>
533
+ */
505
534
private function flattenMimeTypes (array $ responseFormats ): array
506
535
{
507
536
$ responseMimeTypes = [];
@@ -803,7 +832,7 @@ private function appendSchemaDefinitions(\ArrayObject $schemas, \ArrayObject $de
803
832
/**
804
833
* @return array{0: int, 1: Parameter}|null
805
834
*/
806
- private function hasParameter (Model \ Operation $ operation , Parameter $ parameter ): ?array
835
+ private function hasParameter (Operation $ operation , Parameter $ parameter ): ?array
807
836
{
808
837
foreach ($ operation ->getParameters () as $ key => $ existingParameter ) {
809
838
if ($ existingParameter ->getName () === $ parameter ->getName () && $ existingParameter ->getIn () === $ parameter ->getIn ()) {
@@ -843,4 +872,55 @@ private function mergeParameter(Parameter $actual, Parameter $defined): Paramete
843
872
844
873
return $ actual ;
845
874
}
875
+
876
+ /**
877
+ * @param string[] $errors
878
+ * @param array<string, string> $responseMimeTypes
879
+ */
880
+ private function addOperationErrors (Operation $ operation , array $ errors , array $ responseMimeTypes , ResourceMetadataCollection $ resourceMetadataCollection , Schema $ schema , \ArrayObject $ schemas ): Operation
881
+ {
882
+ $ existingResponses = null ;
883
+ foreach ($ errors as $ error ) {
884
+ if (!is_a ($ error , ProblemExceptionInterface::class, true )) {
885
+ throw new RuntimeException (\sprintf ('The error class "%s" does not implement "%s". Did you forget a use statement? ' , $ error , ProblemExceptionInterface::class));
886
+ }
887
+
888
+ $ status = null ;
889
+ $ description = null ;
890
+
891
+ try {
892
+ /** @var ProblemExceptionInterface $exception */
893
+ $ exception = new $ error ();
894
+ $ status = $ exception ->getStatus ();
895
+ $ description = $ exception ->getTitle ();
896
+ } catch (\TypeError ) {
897
+ }
898
+
899
+ try {
900
+ $ errorOperation = $ this ->resourceMetadataFactory ->create ($ error )->getOperation ();
901
+ if (!is_a ($ errorOperation , Error::class)) {
902
+ throw new RuntimeException (\sprintf ('The error class %s is not an ErrorResource ' , $ error ));
903
+ }
904
+ } catch (ResourceClassNotFoundException |OperationNotFoundException ) {
905
+ $ errorOperation = null ;
906
+ }
907
+ $ status ??= $ errorOperation ?->getStatus();
908
+ $ description ??= $ errorOperation ?->getDescription();
909
+
910
+ if (!$ status ) {
911
+ throw new RuntimeException (\sprintf ('The error class %s has no status defined, please either implement ProblemExceptionInterface, or make it an ErrorResource with a status ' , $ error ));
912
+ }
913
+
914
+ $ operationErrorSchemas = [];
915
+ foreach ($ responseMimeTypes as $ operationFormat ) {
916
+ $ operationErrorSchema = $ this ->jsonSchemaFactory ->buildSchema ($ error , $ operationFormat , Schema::TYPE_OUTPUT , null , $ schema );
917
+ $ operationErrorSchemas [$ operationFormat ] = $ operationErrorSchema ;
918
+ $ this ->appendSchemaDefinitions ($ schemas , $ operationErrorSchema ->getDefinitions ());
919
+ }
920
+
921
+ $ operation = $ this ->buildOpenApiResponse ($ existingResponses ??= $ operation ->getResponses () ?: [], $ status , $ description ?? '' , $ operation , $ errorOperation , $ responseMimeTypes , $ operationErrorSchemas , $ resourceMetadataCollection );
922
+ }
923
+
924
+ return $ operation ;
925
+ }
846
926
}
0 commit comments