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

DocumentationNormalizer::getPathOperation()   B

Complexity

Conditions 9
Paths 32

Size

Total Lines 28
Code Lines 21

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 21
dl 0
loc 28
rs 8.0555
c 0
b 0
f 0
cc 9
nc 32
nop 9

How to fix   Many Parameters   

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

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