Passed
Push — master ( c8abf6...ca1612 )
by Kévin
02:37
created

DocumentationNormalizer::updateGetOperation()   F

Complexity

Conditions 17
Paths 276

Size

Total Lines 101
Code Lines 64

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 64
dl 0
loc 101
rs 3.4333
c 0
b 0
f 0
cc 17
nc 276
nop 9

How to fix   Long Method    Complexity    Many Parameters   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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

293
                return $this->updateGetOperation($v3, $pathOperation, $responseMimeTypes ?? $mimeTypes, $operationType, $resourceMetadata, $resourceClass, /** @scrutinizer ignore-type */ $resourceShortName, $operationName, $definitions);
Loading history...
294
            case 'POST':
295
                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

295
                return $this->updatePostOperation($v3, $pathOperation, $responseMimeTypes ?? $mimeTypes, $operationType, $resourceMetadata, $resourceClass, /** @scrutinizer ignore-type */ $resourceShortName, $operationName, $definitions);
Loading history...
296
            case 'PATCH':
297
                $pathOperation['summary'] ?? $pathOperation['summary'] = sprintf('Updates the %s resource.', $resourceShortName);
298
            // no break
299
            case 'PUT':
300
                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

300
                return $this->updatePutOperation($v3, $pathOperation, $responseMimeTypes ?? $mimeTypes, $operationType, $resourceMetadata, $resourceClass, /** @scrutinizer ignore-type */ $resourceShortName, $operationName, $definitions);
Loading history...
301
            case 'DELETE':
302
                return $this->updateDeleteOperation($v3, $pathOperation, $resourceShortName, $operationType, $operationName, $resourceMetadata);
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

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

551
            $definitionKey = $this->getDefinitionKey(/** @scrutinizer ignore-type */ $keyPrefix, (array) ($serializerContext[AbstractNormalizer::GROUPS] ?? []));
Loading history...
552
        }
553
554
        if (!isset($definitions[$definitionKey])) {
555
            $definitions[$definitionKey] = [];  // Initialize first to prevent infinite loop
556
            $definitions[$definitionKey] = $this->getDefinitionSchema($v3, $publicClass ?? $resourceClass, $resourceMetadata, $definitions, $serializerContext);
557
        }
558
559
        return $definitionKey;
560
    }
561
562
    private function getDefinitionKey(string $resourceShortName, array $groups): string
563
    {
564
        return $groups ? sprintf('%s-%s', $resourceShortName, implode('_', $groups)) : $resourceShortName;
565
    }
566
567
    /**
568
     * Gets a definition Schema Object.
569
     *
570
     * @see https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#schemaObject
571
     */
572
    private function getDefinitionSchema(bool $v3, string $resourceClass, ResourceMetadata $resourceMetadata, \ArrayObject $definitions, array $serializerContext = null): \ArrayObject
573
    {
574
        $definitionSchema = new \ArrayObject(['type' => 'object']);
575
576
        if (null !== $description = $resourceMetadata->getDescription()) {
577
            $definitionSchema['description'] = $description;
578
        }
579
580
        if (null !== $iri = $resourceMetadata->getIri()) {
581
            $definitionSchema['externalDocs'] = ['url' => $iri];
582
        }
583
584
        $options = isset($serializerContext[AbstractNormalizer::GROUPS]) ? ['serializer_groups' => $serializerContext[AbstractNormalizer::GROUPS]] : [];
585
        foreach ($this->propertyNameCollectionFactory->create($resourceClass, $options) as $propertyName) {
586
            $propertyMetadata = $this->propertyMetadataFactory->create($resourceClass, $propertyName);
587
            $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

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