Passed
Push — master ( 680a87...20590a )
by Kévin
03:11
created

DocumentationNormalizer::getDefinitionSchema()   B

Complexity

Conditions 7
Paths 40

Size

Total Lines 24
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 13
dl 0
loc 24
rs 8.8333
c 0
b 0
f 0
cc 7
nc 40
nop 5
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\Core\Swagger\Serializer;
15
16
use ApiPlatform\Core\Api\FilterCollection;
17
use ApiPlatform\Core\Api\FilterLocatorTrait;
18
use ApiPlatform\Core\Api\OperationAwareFormatsProviderInterface;
19
use ApiPlatform\Core\Api\OperationMethodResolverInterface;
20
use ApiPlatform\Core\Api\OperationType;
21
use ApiPlatform\Core\Api\ResourceClassResolverInterface;
22
use ApiPlatform\Core\Api\UrlGeneratorInterface;
23
use ApiPlatform\Core\Documentation\Documentation;
24
use ApiPlatform\Core\Metadata\Property\Factory\PropertyMetadataFactoryInterface;
25
use ApiPlatform\Core\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface;
26
use ApiPlatform\Core\Metadata\Property\PropertyMetadata;
27
use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface;
28
use ApiPlatform\Core\Metadata\Resource\ResourceMetadata;
29
use ApiPlatform\Core\Operation\Factory\SubresourceOperationFactoryInterface;
30
use ApiPlatform\Core\PathResolver\OperationPathResolverInterface;
31
use Psr\Container\ContainerInterface;
32
use Symfony\Component\PropertyInfo\Type;
33
use Symfony\Component\Serializer\NameConverter\NameConverterInterface;
34
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
35
use Symfony\Component\Serializer\Normalizer\CacheableSupportsMethodInterface;
36
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
37
38
/**
39
 * Generates an OpenAPI specification (formerly known as Swagger). OpenAPI v2 and v3 are supported.
40
 *
41
 * @author Amrouche Hamza <[email protected]>
42
 * @author Teoh Han Hui <[email protected]>
43
 * @author Kévin Dunglas <[email protected]>
44
 * @author Anthony GRASSIOT <[email protected]>
45
 */
46
final class DocumentationNormalizer implements NormalizerInterface, CacheableSupportsMethodInterface
47
{
48
    use FilterLocatorTrait;
49
50
    const FORMAT = 'json';
51
    const BASE_URL = 'base_url';
52
    const SPEC_VERSION = 'spec_version';
53
    const OPENAPI_VERSION = '3.0.2';
54
    const SWAGGER_DEFINITION_NAME = 'swagger_definition_name';
55
    const SWAGGER_VERSION = '2.0';
56
57
    /**
58
     * @deprecated
59
     */
60
    const ATTRIBUTE_NAME = 'swagger_context';
61
62
    private $resourceMetadataFactory;
63
    private $propertyNameCollectionFactory;
64
    private $propertyMetadataFactory;
65
    private $resourceClassResolver;
66
    private $operationMethodResolver;
67
    private $operationPathResolver;
68
    private $nameConverter;
69
    private $oauthEnabled;
70
    private $oauthType;
71
    private $oauthFlow;
72
    private $oauthTokenUrl;
73
    private $oauthAuthorizationUrl;
74
    private $oauthScopes;
75
    private $apiKeys;
76
    private $subresourceOperationFactory;
77
    private $paginationEnabled;
78
    private $paginationPageParameterName;
79
    private $clientItemsPerPage;
80
    private $itemsPerPageParameterName;
81
    private $paginationClientEnabled;
82
    private $paginationClientEnabledParameterName;
83
    private $formatsProvider;
84
    private $defaultContext = [
85
        self::BASE_URL => '/',
86
        self::SPEC_VERSION => 2,
87
        ApiGatewayNormalizer::API_GATEWAY => false,
88
    ];
89
90
    /**
91
     * @param ContainerInterface|FilterCollection|null $filterLocator The new filter locator or the deprecated filter collection
92
     */
93
    public function __construct(ResourceMetadataFactoryInterface $resourceMetadataFactory, PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, ResourceClassResolverInterface $resourceClassResolver, OperationMethodResolverInterface $operationMethodResolver, OperationPathResolverInterface $operationPathResolver, UrlGeneratorInterface $urlGenerator = null, $filterLocator = null, NameConverterInterface $nameConverter = null, bool $oauthEnabled = false, string $oauthType = '', string $oauthFlow = '', string $oauthTokenUrl = '', string $oauthAuthorizationUrl = '', array $oauthScopes = [], array $apiKeys = [], SubresourceOperationFactoryInterface $subresourceOperationFactory = null, bool $paginationEnabled = true, string $paginationPageParameterName = 'page', bool $clientItemsPerPage = false, string $itemsPerPageParameterName = 'itemsPerPage', OperationAwareFormatsProviderInterface $formatsProvider = null, bool $paginationClientEnabled = false, string $paginationClientEnabledParameterName = 'pagination', array $defaultContext = [])
94
    {
95
        if ($urlGenerator) {
96
            @trigger_error(sprintf('Passing an instance of %s to %s() is deprecated since version 2.1 and will be removed in 3.0.', UrlGeneratorInterface::class, __METHOD__), E_USER_DEPRECATED);
97
        }
98
99
        $this->setFilterLocator($filterLocator, true);
100
101
        $this->resourceMetadataFactory = $resourceMetadataFactory;
102
        $this->propertyNameCollectionFactory = $propertyNameCollectionFactory;
103
        $this->propertyMetadataFactory = $propertyMetadataFactory;
104
        $this->resourceClassResolver = $resourceClassResolver;
105
        $this->operationMethodResolver = $operationMethodResolver;
106
        $this->operationPathResolver = $operationPathResolver;
107
        $this->nameConverter = $nameConverter;
108
        $this->oauthEnabled = $oauthEnabled;
109
        $this->oauthType = $oauthType;
110
        $this->oauthFlow = $oauthFlow;
111
        $this->oauthTokenUrl = $oauthTokenUrl;
112
        $this->oauthAuthorizationUrl = $oauthAuthorizationUrl;
113
        $this->oauthScopes = $oauthScopes;
114
        $this->subresourceOperationFactory = $subresourceOperationFactory;
115
        $this->paginationEnabled = $paginationEnabled;
116
        $this->paginationPageParameterName = $paginationPageParameterName;
117
        $this->apiKeys = $apiKeys;
118
        $this->subresourceOperationFactory = $subresourceOperationFactory;
119
        $this->clientItemsPerPage = $clientItemsPerPage;
120
        $this->itemsPerPageParameterName = $itemsPerPageParameterName;
121
        $this->formatsProvider = $formatsProvider;
122
        $this->paginationClientEnabled = $paginationClientEnabled;
123
        $this->paginationClientEnabledParameterName = $paginationClientEnabledParameterName;
124
125
        $this->defaultContext = array_merge($this->defaultContext, $defaultContext);
126
    }
127
128
    /**
129
     * {@inheritdoc}
130
     */
131
    public function normalize($object, $format = null, array $context = [])
132
    {
133
        $v3 = 3 === ($context['spec_version'] ?? $this->defaultContext['spec_version']) && !($context['api_gateway'] ?? $this->defaultContext['api_gateway']);
134
135
        $mimeTypes = $object->getMimeTypes();
136
        $definitions = new \ArrayObject();
137
        $paths = new \ArrayObject();
138
139
        foreach ($object->getResourceNameCollection() as $resourceClass) {
140
            $resourceMetadata = $this->resourceMetadataFactory->create($resourceClass);
141
            $resourceShortName = $resourceMetadata->getShortName();
142
143
            $this->addPaths($v3, $paths, $definitions, $resourceClass, $resourceShortName, $resourceMetadata, $mimeTypes, OperationType::COLLECTION);
0 ignored issues
show
Bug introduced by
It seems like $resourceShortName can also be of type null; however, parameter $resourceShortName of ApiPlatform\Core\Swagger...nNormalizer::addPaths() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

143
            $this->addPaths($v3, $paths, $definitions, $resourceClass, /** @scrutinizer ignore-type */ $resourceShortName, $resourceMetadata, $mimeTypes, OperationType::COLLECTION);
Loading history...
144
            $this->addPaths($v3, $paths, $definitions, $resourceClass, $resourceShortName, $resourceMetadata, $mimeTypes, OperationType::ITEM);
145
146
            if (null === $this->subresourceOperationFactory) {
147
                continue;
148
            }
149
150
            foreach ($this->subresourceOperationFactory->create($resourceClass) as $operationId => $subresourceOperation) {
151
                $operationName = 'get';
152
                $subResourceMetadata = $this->resourceMetadataFactory->create($subresourceOperation['resource_class']);
153
                $serializerContext = $this->getSerializerContext(OperationType::SUBRESOURCE, false, $subResourceMetadata, $operationName);
154
                $responseDefinitionKey = $this->getDefinition($v3, $definitions, $subResourceMetadata, $subresourceOperation['resource_class'], $serializerContext);
155
156
                $pathOperation = new \ArrayObject([]);
157
                $pathOperation['tags'] = $subresourceOperation['shortNames'];
158
                $pathOperation['operationId'] = $operationId;
159
160
                if (null !== $this->formatsProvider) {
161
                    $responseFormats = $this->formatsProvider->getFormatsFromOperation($subresourceOperation['resource_class'], $operationName, OperationType::SUBRESOURCE);
162
                    $responseMimeTypes = $this->extractMimeTypes($responseFormats);
163
                }
164
                if (!$v3) {
165
                    $pathOperation['produces'] = $responseMimeTypes ?? $mimeTypes;
166
                }
167
168
                if ($subresourceOperation['collection']) {
169
                    $baseSuccessResponse = ['description' => sprintf('%s collection response', $subresourceOperation['shortNames'][0])];
170
171
                    if ($v3) {
172
                        $baseSuccessResponse['content'] = array_fill_keys($responseMimeTypes ?? $mimeTypes, ['schema' => ['type' => 'array', 'items' => ['$ref' => sprintf('#/components/schemas/%s', $responseDefinitionKey)]]]);
173
                    } else {
174
                        $baseSuccessResponse['schema'] = ['type' => 'array', 'items' => ['$ref' => sprintf('#/definitions/%s', $responseDefinitionKey)]];
175
                    }
176
                } else {
177
                    $baseSuccessResponse = ['description' => sprintf('%s resource response', $subresourceOperation['shortNames'][0])];
178
179
                    if ($v3) {
180
                        $baseSuccessResponse['content'] = array_fill_keys($responseMimeTypes ?? $mimeTypes, ['schema' => ['$ref' => sprintf('#/components/schemas/%s', $responseDefinitionKey)]]);
181
                    } else {
182
                        $baseSuccessResponse['schema'] = ['$ref' => sprintf('#/definitions/%s', $responseDefinitionKey)];
183
                    }
184
                }
185
186
                $pathOperation['summary'] = sprintf('Retrieves %s%s resource%s.', $subresourceOperation['collection'] ? 'the collection of ' : 'a ', $subresourceOperation['shortNames'][0], $subresourceOperation['collection'] ? 's' : '');
187
                $pathOperation['responses'] = [
188
                    '200' => $baseSuccessResponse,
189
                    '404' => ['description' => 'Resource not found'],
190
                ];
191
192
                // Avoid duplicates parameters when there is a filter on a subresource identifier
193
                $parametersMemory = [];
194
                $pathOperation['parameters'] = [];
195
196
                foreach ($subresourceOperation['identifiers'] as list($identifier, , $hasIdentifier)) {
197
                    if (true === $hasIdentifier) {
198
                        $parameter = [
199
                            'name' => $identifier,
200
                            'in' => 'path',
201
                            'required' => true,
202
                        ];
203
                        $v3 ? $parameter['schema'] = ['type' => 'string'] : $parameter['type'] = 'string';
204
205
                        $pathOperation['parameters'][] = $parameter;
206
                        $parametersMemory[] = $identifier;
207
                    }
208
                }
209
210
                if ($parameters = $this->getFiltersParameters($v3, $subresourceOperation['resource_class'], $operationName, $subResourceMetadata, $definitions, $serializerContext)) {
211
                    foreach ($parameters as $parameter) {
212
                        if (!\in_array($parameter['name'], $parametersMemory, true)) {
213
                            $pathOperation['parameters'][] = $parameter;
214
                        }
215
                    }
216
                }
217
218
                $paths[$this->getPath($subresourceOperation['shortNames'][0], $subresourceOperation['route_name'], $subresourceOperation, OperationType::SUBRESOURCE)] = new \ArrayObject(['get' => $pathOperation]);
219
            }
220
        }
221
222
        $definitions->ksort();
223
        $paths->ksort();
224
225
        return $this->computeDoc($v3, $object, $definitions, $paths, $context);
226
    }
227
228
    /**
229
     * Updates the list of entries in the paths collection.
230
     */
231
    private function addPaths(bool $v3, \ArrayObject $paths, \ArrayObject $definitions, string $resourceClass, string $resourceShortName, ResourceMetadata $resourceMetadata, array $mimeTypes, string $operationType)
232
    {
233
        if (null === $operations = OperationType::COLLECTION === $operationType ? $resourceMetadata->getCollectionOperations() : $resourceMetadata->getItemOperations()) {
234
            return;
235
        }
236
237
        foreach ($operations as $operationName => $operation) {
238
            $path = $this->getPath($resourceShortName, $operationName, $operation, $operationType);
239
            $method = OperationType::ITEM === $operationType ? $this->operationMethodResolver->getItemOperationMethod($resourceClass, $operationName) : $this->operationMethodResolver->getCollectionOperationMethod($resourceClass, $operationName);
240
241
            $paths[$path][strtolower($method)] = $this->getPathOperation($v3, $operationName, $operation, $method, $operationType, $resourceClass, $resourceMetadata, $mimeTypes, $definitions);
242
        }
243
    }
244
245
    /**
246
     * Gets the path for an operation.
247
     *
248
     * If the path ends with the optional _format parameter, it is removed
249
     * as optional path parameters are not yet supported.
250
     *
251
     * @see https://github.com/OAI/OpenAPI-Specification/issues/93
252
     */
253
    private function getPath(string $resourceShortName, string $operationName, array $operation, string $operationType): string
254
    {
255
        $path = $this->operationPathResolver->resolveOperationPath($resourceShortName, $operation, $operationType, $operationName);
256
        if ('.{_format}' === substr($path, -10)) {
257
            $path = substr($path, 0, -10);
258
        }
259
260
        return $path;
261
    }
262
263
    /**
264
     * Gets a path Operation Object.
265
     *
266
     * @see https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#operation-object
267
     *
268
     * @param string[] $mimeTypes
269
     */
270
    private function getPathOperation(bool $v3, string $operationName, array $operation, string $method, string $operationType, string $resourceClass, ResourceMetadata $resourceMetadata, array $mimeTypes, \ArrayObject $definitions): \ArrayObject
271
    {
272
        $pathOperation = new \ArrayObject($operation[$v3 ? 'openapi_context' : 'swagger_context'] ?? []);
273
        $resourceShortName = $resourceMetadata->getShortName();
274
        $pathOperation['tags'] ?? $pathOperation['tags'] = [$resourceShortName];
275
        $pathOperation['operationId'] ?? $pathOperation['operationId'] = lcfirst($operationName).ucfirst($resourceShortName).ucfirst($operationType);
276
        if ($resourceMetadata->getTypedOperationAttribute($operationType, $operationName, 'deprecation_reason', null, true)) {
277
            $pathOperation['deprecated'] = true;
278
        }
279
        if (null !== $this->formatsProvider) {
280
            $responseFormats = $this->formatsProvider->getFormatsFromOperation($resourceClass, $operationName, $operationType);
281
            $responseMimeTypes = $this->extractMimeTypes($responseFormats);
282
        }
283
        switch ($method) {
284
            case 'GET':
285
                return $this->updateGetOperation($v3, $pathOperation, $responseMimeTypes ?? $mimeTypes, $operationType, $resourceMetadata, $resourceClass, $resourceShortName, $operationName, $definitions);
0 ignored issues
show
Bug introduced by
It seems like $resourceShortName can also be of type null; however, parameter $resourceShortName of ApiPlatform\Core\Swagger...r::updateGetOperation() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

285
                return $this->updateGetOperation($v3, $pathOperation, $responseMimeTypes ?? $mimeTypes, $operationType, $resourceMetadata, $resourceClass, /** @scrutinizer ignore-type */ $resourceShortName, $operationName, $definitions);
Loading history...
286
            case 'POST':
287
                return $this->updatePostOperation($v3, $pathOperation, $responseMimeTypes ?? $mimeTypes, $operationType, $resourceMetadata, $resourceClass, $resourceShortName, $operationName, $definitions);
0 ignored issues
show
Bug introduced by
It seems like $resourceShortName can also be of type null; however, parameter $resourceShortName of ApiPlatform\Core\Swagger...::updatePostOperation() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

287
                return $this->updatePostOperation($v3, $pathOperation, $responseMimeTypes ?? $mimeTypes, $operationType, $resourceMetadata, $resourceClass, /** @scrutinizer ignore-type */ $resourceShortName, $operationName, $definitions);
Loading history...
288
            case 'PATCH':
289
                $pathOperation['summary'] ?? $pathOperation['summary'] = sprintf('Updates the %s resource.', $resourceShortName);
290
            // no break
291
            case 'PUT':
292
                return $this->updatePutOperation($v3, $pathOperation, $responseMimeTypes ?? $mimeTypes, $operationType, $resourceMetadata, $resourceClass, $resourceShortName, $operationName, $definitions);
0 ignored issues
show
Bug introduced by
It seems like $resourceShortName can also be of type null; however, parameter $resourceShortName of ApiPlatform\Core\Swagger...r::updatePutOperation() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

292
                return $this->updatePutOperation($v3, $pathOperation, $responseMimeTypes ?? $mimeTypes, $operationType, $resourceMetadata, $resourceClass, /** @scrutinizer ignore-type */ $resourceShortName, $operationName, $definitions);
Loading history...
293
            case 'DELETE':
294
                return $this->updateDeleteOperation($v3, $pathOperation, $resourceShortName);
0 ignored issues
show
Bug introduced by
It seems like $resourceShortName can also be of type null; however, parameter $resourceShortName of ApiPlatform\Core\Swagger...updateDeleteOperation() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

294
                return $this->updateDeleteOperation($v3, $pathOperation, /** @scrutinizer ignore-type */ $resourceShortName);
Loading history...
295
        }
296
297
        return $pathOperation;
298
    }
299
300
    private function updateGetOperation(bool $v3, \ArrayObject $pathOperation, array $mimeTypes, string $operationType, ResourceMetadata $resourceMetadata, string $resourceClass, string $resourceShortName, string $operationName, \ArrayObject $definitions): \ArrayObject
301
    {
302
        $serializerContext = $this->getSerializerContext($operationType, false, $resourceMetadata, $operationName);
303
        $responseDefinitionKey = $this->getDefinition($v3, $definitions, $resourceMetadata, $resourceClass, $serializerContext);
304
305
        if (!$v3) {
306
            $pathOperation['produces'] ?? $pathOperation['produces'] = $mimeTypes;
307
        }
308
309
        if (OperationType::COLLECTION === $operationType) {
310
            $pathOperation['summary'] ?? $pathOperation['summary'] = sprintf('Retrieves the collection of %s resources.', $resourceShortName);
311
312
            $successResponse = ['description' => sprintf('%s collection response', $resourceShortName)];
313
            if ($v3) {
314
                $successResponse['content'] = array_fill_keys($mimeTypes, [
315
                    'schema' => [
316
                        'type' => 'array',
317
                        'items' => ['$ref' => sprintf('#/components/schemas/%s', $responseDefinitionKey)],
318
                    ],
319
                ]);
320
            } else {
321
                $successResponse['schema'] = [
322
                    'type' => 'array',
323
                    'items' => ['$ref' => sprintf('#/definitions/%s', $responseDefinitionKey)],
324
                ];
325
            }
326
327
            $pathOperation['responses'] ?? $pathOperation['responses'] = [
328
                '200' => $successResponse,
329
            ];
330
331
            $pathOperation['parameters'] ?? $pathOperation['parameters'] = $this->getFiltersParameters($v3, $resourceClass, $operationName, $resourceMetadata, $definitions, $serializerContext);
332
333
            if ($this->paginationEnabled && $resourceMetadata->getCollectionOperationAttribute($operationName, 'pagination_enabled', true, true)) {
334
                $paginationParameter = [
335
                    'name' => $this->paginationPageParameterName,
336
                    'in' => 'query',
337
                    'required' => false,
338
                    'description' => 'The collection page number',
339
                ];
340
                $v3 ? $paginationParameter['schema'] = ['type' => 'integer'] : $paginationParameter['type'] = 'integer';
341
                $pathOperation['parameters'][] = $paginationParameter;
342
343
                if ($resourceMetadata->getCollectionOperationAttribute($operationName, 'pagination_client_items_per_page', $this->clientItemsPerPage, true)) {
344
                    $itemPerPageParameter = [
345
                        'name' => $this->itemsPerPageParameterName,
346
                        'in' => 'query',
347
                        'required' => false,
348
                        'description' => 'The number of items per page',
349
                    ];
350
                    $v3 ? $itemPerPageParameter['schema'] = ['type' => 'integer'] : $itemPerPageParameter['type'] = 'integer';
351
352
                    $pathOperation['parameters'][] = $itemPerPageParameter;
353
                }
354
            }
355
356
            if ($this->paginationEnabled && $resourceMetadata->getCollectionOperationAttribute($operationName, 'pagination_client_enabled', $this->paginationClientEnabled, true)) {
357
                $paginationEnabledParameter = [
358
                    'name' => $this->paginationClientEnabledParameterName,
359
                    'in' => 'query',
360
                    'required' => false,
361
                    'description' => 'Enable or disable pagination',
362
                ];
363
                $v3 ? $paginationEnabledParameter['schema'] = ['type' => 'boolean'] : $paginationEnabledParameter['type'] = 'boolean';
364
                $pathOperation['parameters'][] = $paginationEnabledParameter;
365
            }
366
367
            return $pathOperation;
368
        }
369
370
        $pathOperation['summary'] ?? $pathOperation['summary'] = sprintf('Retrieves a %s resource.', $resourceShortName);
371
372
        $parameter = [
373
            'name' => 'id',
374
            'in' => 'path',
375
            'required' => true,
376
        ];
377
        $v3 ? $parameter['schema'] = ['type' => 'string'] : $parameter['type'] = 'string';
378
        $pathOperation['parameters'] ?? $pathOperation['parameters'] = [$parameter];
379
380
        $successResponse = ['description' => sprintf('%s resource response', $resourceShortName)];
381
        if ($v3) {
382
            $successResponse['content'] = array_fill_keys($mimeTypes, ['schema' => ['$ref' => sprintf('#/components/schemas/%s', $responseDefinitionKey)]]);
383
        } else {
384
            $successResponse['schema'] = ['$ref' => sprintf('#/definitions/%s', $responseDefinitionKey)];
385
        }
386
387
        $pathOperation['responses'] ?? $pathOperation['responses'] = [
388
            '200' => $successResponse,
389
            '404' => ['description' => 'Resource not found'],
390
        ];
391
392
        return $pathOperation;
393
    }
394
395
    private function updatePostOperation(bool $v3, \ArrayObject $pathOperation, array $mimeTypes, string $operationType, ResourceMetadata $resourceMetadata, string $resourceClass, string $resourceShortName, string $operationName, \ArrayObject $definitions): \ArrayObject
396
    {
397
        if (!$v3) {
398
            $pathOperation['consumes'] ?? $pathOperation['consumes'] = $mimeTypes;
399
            $pathOperation['produces'] ?? $pathOperation['produces'] = $mimeTypes;
400
        }
401
402
        $pathOperation['summary'] ?? $pathOperation['summary'] = sprintf('Creates a %s resource.', $resourceShortName);
403
404
        $successResponse = ['description' => sprintf('%s resource created', $resourceShortName)];
405
406
        if ($v3) {
407
            $successResponse['content'] = array_fill_keys($mimeTypes, [
408
                'schema' => ['$ref' => sprintf('#/components/schemas/%s', $this->getDefinition($v3, $definitions, $resourceMetadata, $resourceClass,
409
                    $this->getSerializerContext($operationType, false, $resourceMetadata, $operationName))),
410
                ],
411
            ]);
412
        } else {
413
            $successResponse['schema'] = ['$ref' => sprintf('#/definitions/%s', $this->getDefinition($v3, $definitions, $resourceMetadata, $resourceClass,
414
                $this->getSerializerContext($operationType, false, $resourceMetadata, $operationName)
415
            ))];
416
        }
417
418
        $pathOperation['responses'] ?? $pathOperation['responses'] = [
419
            '201' => $successResponse,
420
            '400' => ['description' => 'Invalid input'],
421
            '404' => ['description' => 'Resource not found'],
422
        ];
423
424
        if ($v3) {
425
            $pathOperation['requestBody'] ?? $pathOperation['requestBody'] = [
426
                'content' => array_fill_keys($mimeTypes, [
427
                    'schema' => ['$ref' => sprintf('#/components/schemas/%s', $this->getDefinition($v3, $definitions, $resourceMetadata, $resourceClass,
428
                        $this->getSerializerContext($operationType, true, $resourceMetadata, $operationName))),
429
                    ],
430
                ]),
431
                'description' => sprintf('The new %s resource', $resourceShortName),
432
            ];
433
        } else {
434
            $pathOperation['parameters'] ?? $pathOperation['parameters'] = [[
435
                'name' => lcfirst($resourceShortName),
436
                'in' => 'body',
437
                'description' => sprintf('The new %s resource', $resourceShortName),
438
                'schema' => ['$ref' => sprintf('#/definitions/%s', $this->getDefinition($v3, $definitions, $resourceMetadata, $resourceClass,
439
                    $this->getSerializerContext($operationType, true, $resourceMetadata, $operationName)
440
                ))],
441
            ]];
442
        }
443
444
        return $pathOperation;
445
    }
446
447
    private function updatePutOperation(bool $v3, \ArrayObject $pathOperation, array $mimeTypes, string $operationType, ResourceMetadata $resourceMetadata, string $resourceClass, string $resourceShortName, string $operationName, \ArrayObject $definitions): \ArrayObject
448
    {
449
        if (!$v3) {
450
            $pathOperation['consumes'] ?? $pathOperation['consumes'] = $mimeTypes;
451
            $pathOperation['produces'] ?? $pathOperation['produces'] = $mimeTypes;
452
        }
453
454
        $pathOperation['summary'] ?? $pathOperation['summary'] = sprintf('Replaces the %s resource.', $resourceShortName);
455
456
        $parameter = [
457
            'name' => 'id',
458
            'in' => 'path',
459
            'required' => true,
460
        ];
461
        $v3 ? $parameter['schema'] = ['type' => 'string'] : $parameter['type'] = 'string';
462
        $pathOperation['parameters'] ?? $pathOperation['parameters'] = [$parameter];
463
464
        $successResponse = ['description' => sprintf('%s resource updated', $resourceShortName)];
465
        if ($v3) {
466
            $successResponse['content'] = array_fill_keys($mimeTypes, [
467
                'schema' => ['$ref' => sprintf('#/components/schemas/%s', $this->getDefinition($v3, $definitions, $resourceMetadata, $resourceClass,
468
                    $this->getSerializerContext($operationType, false, $resourceMetadata, $operationName))),
469
                ],
470
            ]);
471
        } else {
472
            $successResponse['schema'] = ['$ref' => sprintf('#/definitions/%s', $this->getDefinition($v3, $definitions, $resourceMetadata, $resourceClass, $this->getSerializerContext($operationType, false, $resourceMetadata, $operationName)))];
473
        }
474
475
        $pathOperation['responses'] ?? $pathOperation['responses'] = [
476
            '200' => $successResponse,
477
            '400' => ['description' => 'Invalid input'],
478
            '404' => ['description' => 'Resource not found'],
479
        ];
480
481
        if ($v3) {
482
            $pathOperation['requestBody'] ?? $pathOperation['requestBody'] = [
483
                'content' => array_fill_keys($mimeTypes, [
484
                    'schema' => ['$ref' => sprintf('#/components/schemas/%s', $this->getDefinition($v3, $definitions, $resourceMetadata, $resourceClass,
485
                        $this->getSerializerContext($operationType, true, $resourceMetadata, $operationName))),
486
                    ],
487
                ]),
488
                'description' => sprintf('The updated %s resource', $resourceShortName),
489
            ];
490
        } else {
491
            $pathOperation['parameters'][] = [
492
                'name' => lcfirst($resourceShortName),
493
                'in' => 'body',
494
                'description' => sprintf('The updated %s resource', $resourceShortName),
495
                'schema' => ['$ref' => sprintf('#/definitions/%s', $this->getDefinition($v3, $definitions, $resourceMetadata, $resourceClass,
496
                    $this->getSerializerContext($operationType, true, $resourceMetadata, $operationName)
497
                ))],
498
            ];
499
        }
500
501
        return $pathOperation;
502
    }
503
504
    private function updateDeleteOperation(bool $v3, \ArrayObject $pathOperation, string $resourceShortName): \ArrayObject
505
    {
506
        $pathOperation['summary'] ?? $pathOperation['summary'] = sprintf('Removes the %s resource.', $resourceShortName);
507
        $pathOperation['responses'] ?? $pathOperation['responses'] = [
508
            '204' => ['description' => sprintf('%s resource deleted', $resourceShortName)],
509
            '404' => ['description' => 'Resource not found'],
510
        ];
511
512
        $parameter = [
513
            'name' => 'id',
514
            'in' => 'path',
515
            'required' => true,
516
        ];
517
        $v3 ? $parameter['schema'] = ['type' => 'string'] : $parameter['type'] = 'string';
518
519
        $pathOperation['parameters'] ?? $pathOperation['parameters'] = [$parameter];
520
521
        return $pathOperation;
522
    }
523
524
    private function getDefinition(bool $v3, \ArrayObject $definitions, ResourceMetadata $resourceMetadata, string $resourceClass, array $serializerContext = null): string
525
    {
526
        if (isset($serializerContext[self::SWAGGER_DEFINITION_NAME])) {
527
            $definitionKey = sprintf('%s-%s', $resourceMetadata->getShortName(), $serializerContext[self::SWAGGER_DEFINITION_NAME]);
528
        } else {
529
            $definitionKey = $this->getDefinitionKey($resourceMetadata->getShortName(), (array) ($serializerContext[AbstractNormalizer::GROUPS] ?? []));
0 ignored issues
show
Bug introduced by
It seems like $resourceMetadata->getShortName() can also be of type null; however, parameter $resourceShortName of ApiPlatform\Core\Swagger...zer::getDefinitionKey() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

529
            $definitionKey = $this->getDefinitionKey(/** @scrutinizer ignore-type */ $resourceMetadata->getShortName(), (array) ($serializerContext[AbstractNormalizer::GROUPS] ?? []));
Loading history...
530
        }
531
532
        if (!isset($definitions[$definitionKey])) {
533
            $definitions[$definitionKey] = [];  // Initialize first to prevent infinite loop
534
            $definitions[$definitionKey] = $this->getDefinitionSchema($v3, $resourceClass, $resourceMetadata, $definitions, $serializerContext);
535
        }
536
537
        return $definitionKey;
538
    }
539
540
    private function getDefinitionKey(string $resourceShortName, array $groups): string
541
    {
542
        return $groups ? sprintf('%s-%s', $resourceShortName, implode('_', $groups)) : $resourceShortName;
543
    }
544
545
    /**
546
     * Gets a definition Schema Object.
547
     *
548
     * @see https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#schemaObject
549
     */
550
    private function getDefinitionSchema(bool $v3, string $resourceClass, ResourceMetadata $resourceMetadata, \ArrayObject $definitions, array $serializerContext = null): \ArrayObject
551
    {
552
        $definitionSchema = new \ArrayObject(['type' => 'object']);
553
554
        if (null !== $description = $resourceMetadata->getDescription()) {
555
            $definitionSchema['description'] = $description;
556
        }
557
558
        if (null !== $iri = $resourceMetadata->getIri()) {
559
            $definitionSchema['externalDocs'] = ['url' => $iri];
560
        }
561
562
        $options = isset($serializerContext[AbstractNormalizer::GROUPS]) ? ['serializer_groups' => $serializerContext[AbstractNormalizer::GROUPS]] : [];
563
        foreach ($this->propertyNameCollectionFactory->create($resourceClass, $options) as $propertyName) {
564
            $propertyMetadata = $this->propertyMetadataFactory->create($resourceClass, $propertyName);
565
            $normalizedPropertyName = $this->nameConverter ? $this->nameConverter->normalize($propertyName, $resourceClass, self::FORMAT, $serializerContext ?? []) : $propertyName;
0 ignored issues
show
Unused Code introduced by
The call to Symfony\Component\Serial...rInterface::normalize() has too many arguments starting with $resourceClass. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

565
            $normalizedPropertyName = $this->nameConverter ? $this->nameConverter->/** @scrutinizer ignore-call */ normalize($propertyName, $resourceClass, self::FORMAT, $serializerContext ?? []) : $propertyName;

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
566
            if ($propertyMetadata->isRequired()) {
567
                $definitionSchema['required'][] = $normalizedPropertyName;
568
            }
569
570
            $definitionSchema['properties'][$normalizedPropertyName] = $this->getPropertySchema($v3, $propertyMetadata, $definitions, $serializerContext);
571
        }
572
573
        return $definitionSchema;
574
    }
575
576
    /**
577
     * Gets a property Schema Object.
578
     *
579
     * @see https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#schemaObject
580
     */
581
    private function getPropertySchema(bool $v3, PropertyMetadata $propertyMetadata, \ArrayObject $definitions, array $serializerContext = null): \ArrayObject
582
    {
583
        $propertySchema = new \ArrayObject($propertyMetadata->getAttributes()[$v3 ? 'openapi_context' : 'swagger_context'] ?? []);
584
585
        if (false === $propertyMetadata->isWritable() && !$propertyMetadata->isInitializable()) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $propertyMetadata->isInitializable() of type boolean|null is loosely compared to false; this is ambiguous if the boolean can be false. You might want to explicitly use !== null instead.

If an expression can have both false, and null as possible values. It is generally a good practice to always use strict comparison to clearly distinguish between those two values.

$a = canBeFalseAndNull();

// Instead of
if ( ! $a) { }

// Better use one of the explicit versions:
if ($a !== null) { }
if ($a !== false) { }
if ($a !== null && $a !== false) { }
Loading history...
586
            $propertySchema['readOnly'] = true;
587
        }
588
589
        if (null !== $description = $propertyMetadata->getDescription()) {
590
            $propertySchema['description'] = $description;
591
        }
592
593
        if (null === $type = $propertyMetadata->getType()) {
594
            return $propertySchema;
595
        }
596
597
        $isCollection = $type->isCollection();
598
        if (null === $valueType = $isCollection ? $type->getCollectionValueType() : $type) {
599
            $builtinType = 'string';
600
            $className = null;
601
        } else {
602
            $builtinType = $valueType->getBuiltinType();
603
            $className = $valueType->getClassName();
604
        }
605
606
        $valueSchema = $this->getType($v3, $builtinType, $isCollection, $className, $propertyMetadata->isReadableLink(), $definitions, $serializerContext);
607
608
        return new \ArrayObject((array) $propertySchema + $valueSchema);
609
    }
610
611
    /**
612
     * Gets the Swagger's type corresponding to the given PHP's type.
613
     */
614
    private function getType(bool $v3, string $type, bool $isCollection, string $className = null, bool $readableLink = null, \ArrayObject $definitions, array $serializerContext = null): array
615
    {
616
        if ($isCollection) {
617
            return ['type' => 'array', 'items' => $this->getType($v3, $type, false, $className, $readableLink, $definitions, $serializerContext)];
618
        }
619
620
        if (Type::BUILTIN_TYPE_STRING === $type) {
621
            return ['type' => 'string'];
622
        }
623
624
        if (Type::BUILTIN_TYPE_INT === $type) {
625
            return ['type' => 'integer'];
626
        }
627
628
        if (Type::BUILTIN_TYPE_FLOAT === $type) {
629
            return ['type' => 'number'];
630
        }
631
632
        if (Type::BUILTIN_TYPE_BOOL === $type) {
633
            return ['type' => 'boolean'];
634
        }
635
636
        if (Type::BUILTIN_TYPE_OBJECT === $type) {
637
            if (null === $className) {
638
                return ['type' => 'string'];
639
            }
640
641
            if (is_subclass_of($className, \DateTimeInterface::class)) {
642
                return ['type' => 'string', 'format' => 'date-time'];
643
            }
644
645
            if (!$this->resourceClassResolver->isResourceClass($className)) {
646
                return ['type' => 'string'];
647
            }
648
649
            if (true === $readableLink) {
650
                return [
651
                    '$ref' => sprintf(
652
                        $v3 ? '#/components/schemas/%s' : '#/definitions/%s',
653
                        $this->getDefinition($v3, $definitions, $this->resourceMetadataFactory->create($className), $className, $serializerContext)
654
                    ),
655
                ];
656
            }
657
        }
658
659
        return ['type' => 'string'];
660
    }
661
662
    private function computeDoc(bool $v3, Documentation $documentation, \ArrayObject $definitions, \ArrayObject $paths, array $context): array
663
    {
664
        if ($v3) {
665
            $docs = [
666
                'openapi' => self::OPENAPI_VERSION,
667
                'servers' => [['url' => $context[self::BASE_URL] ?? $this->defaultContext[self::BASE_URL]]],
668
            ];
669
        } else {
670
            $docs = [
671
                'swagger' => self::SWAGGER_VERSION,
672
                'basePath' => $context[self::BASE_URL] ?? $this->defaultContext[self::BASE_URL],
673
            ];
674
        }
675
676
        $docs += [
677
            'info' => [
678
                'title' => $documentation->getTitle(),
679
                'version' => $documentation->getVersion(),
680
            ],
681
            'paths' => $paths,
682
        ];
683
684
        if ('' !== $description = $documentation->getDescription()) {
685
            $docs['info']['description'] = $description;
686
        }
687
688
        $securityDefinitions = [];
689
        $security = [];
690
691
        if ($this->oauthEnabled) {
692
            $securityDefinitions['oauth'] = [
693
                'type' => $this->oauthType,
694
                'description' => 'OAuth client_credentials Grant',
695
                'flow' => $this->oauthFlow,
696
                'tokenUrl' => $this->oauthTokenUrl,
697
                'authorizationUrl' => $this->oauthAuthorizationUrl,
698
                'scopes' => $this->oauthScopes,
699
            ];
700
701
            $security[] = ['oauth' => []];
702
        }
703
704
        foreach ($this->apiKeys as $key => $apiKey) {
705
            $name = $apiKey['name'];
706
            $type = $apiKey['type'];
707
708
            $securityDefinitions[$key] = [
709
                'type' => 'apiKey',
710
                'in' => $type,
711
                'description' => sprintf('Value for the %s %s', $name, 'query' === $type ? sprintf('%s parameter', $type) : $type),
712
                'name' => $name,
713
            ];
714
715
            $security[] = [$key => []];
716
        }
717
718
        if ($v3) {
719
            if ($securityDefinitions && $security) {
720
                $docs['security'] = $security;
721
            }
722
        } elseif ($securityDefinitions && $security) {
723
            $docs['securityDefinitions'] = $securityDefinitions;
724
            $docs['security'] = $security;
725
        }
726
727
        if ($v3) {
728
            if (\count($definitions) + \count($securityDefinitions)) {
729
                $docs['components'] = [];
730
                if (\count($definitions)) {
731
                    $docs['components']['schemas'] = $definitions;
732
                }
733
                if (\count($securityDefinitions)) {
734
                    $docs['components']['securitySchemes'] = $securityDefinitions;
735
                }
736
            }
737
        } elseif (\count($definitions) > 0) {
738
            $docs['definitions'] = $definitions;
739
        }
740
741
        return $docs;
742
    }
743
744
    /**
745
     * Gets parameters corresponding to enabled filters.
746
     */
747
    private function getFiltersParameters(bool $v3, string $resourceClass, string $operationName, ResourceMetadata $resourceMetadata, \ArrayObject $definitions, array $serializerContext = null): array
748
    {
749
        if (null === $this->filterLocator) {
750
            return [];
751
        }
752
753
        $parameters = [];
754
        $resourceFilters = $resourceMetadata->getCollectionOperationAttribute($operationName, 'filters', [], true);
755
        foreach ($resourceFilters as $filterId) {
756
            if (!$filter = $this->getFilter($filterId)) {
757
                continue;
758
            }
759
760
            foreach ($filter->getDescription($resourceClass) as $name => $data) {
761
                $parameter = [
762
                    'name' => $name,
763
                    'in' => 'query',
764
                    'required' => $data['required'],
765
                ];
766
767
                $type = $this->getType($v3, $data['type'], $data['is_collection'] ?? false, null, null, $definitions, $serializerContext);
768
                $v3 ? $parameter['schema'] = $type : $parameter += $type;
769
770
                if ('array' === $type['type'] ?? '') {
771
                    $deepObject = \in_array($data['type'], [Type::BUILTIN_TYPE_ARRAY, Type::BUILTIN_TYPE_OBJECT], true);
772
773
                    if ($v3) {
774
                        $parameter['style'] = $deepObject ? 'deepObject' : 'form';
775
                        $parameter['explode'] = true;
776
                    } else {
777
                        $parameter['collectionFormat'] = $deepObject ? 'csv' : 'multi';
778
                    }
779
                }
780
781
                $key = $v3 ? 'openapi' : 'swagger';
782
                if (isset($data[$key])) {
783
                    $parameter = $data[$key] + $parameter;
784
                }
785
786
                $parameters[] = $parameter;
787
            }
788
        }
789
790
        return $parameters;
791
    }
792
793
    /**
794
     * {@inheritdoc}
795
     */
796
    public function supportsNormalization($data, $format = null)
797
    {
798
        return self::FORMAT === $format && $data instanceof Documentation;
799
    }
800
801
    /**
802
     * {@inheritdoc}
803
     */
804
    public function hasCacheableSupportsMethod(): bool
805
    {
806
        return true;
807
    }
808
809
    private function getSerializerContext(string $operationType, bool $denormalization, ResourceMetadata $resourceMetadata, string $operationName): ?array
810
    {
811
        $contextKey = $denormalization ? 'denormalization_context' : 'normalization_context';
812
813
        if (OperationType::COLLECTION === $operationType) {
814
            return $resourceMetadata->getCollectionOperationAttribute($operationName, $contextKey, null, true);
815
        }
816
817
        return $resourceMetadata->getItemOperationAttribute($operationName, $contextKey, null, true);
818
    }
819
820
    private function extractMimeTypes(array $responseFormats): array
821
    {
822
        $responseMimeTypes = [];
823
        foreach ($responseFormats as $mimeTypes) {
824
            foreach ($mimeTypes as $mimeType) {
825
                $responseMimeTypes[] = $mimeType;
826
            }
827
        }
828
829
        return $responseMimeTypes;
830
    }
831
}
832