Passed
Push — master ( 9a5b6f...aff44c )
by Kévin
05:40 queued 02:53
created

DocumentationNormalizer::updateGetOperation()   B

Complexity

Conditions 7
Paths 7

Size

Total Lines 57
Code Lines 33

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 33
dl 0
loc 57
rs 8.4586
c 0
b 0
f 0
cc 7
nc 7
nop 8

How to fix   Long Method    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\OpenApi\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\Documentation\Documentation;
23
use ApiPlatform\Core\Metadata\Property\Factory\PropertyMetadataFactoryInterface;
24
use ApiPlatform\Core\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface;
25
use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface;
26
use ApiPlatform\Core\Metadata\Resource\ResourceMetadata;
27
use ApiPlatform\Core\Operation\Factory\SubresourceOperationFactoryInterface;
28
use ApiPlatform\Core\PathResolver\OperationPathResolverInterface;
29
use Psr\Container\ContainerInterface;
30
use Symfony\Component\PropertyInfo\Type;
31
use Symfony\Component\Serializer\NameConverter\NameConverterInterface;
32
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
33
34
/**
35
 * Creates a machine readable OpenAPI 3.0 documentation.
36
 *
37
 * @experimental
38
 *
39
 * @author Anthony GRASSIOT <[email protected]>
40
 */
41
final class DocumentationNormalizer extends AbstractDocumentationNormalizer
42
{
43
    use FilterLocatorTrait;
44
45
    const OPENAPI_VERSION = '3.0.2';
46
    const SWAGGER_DEFINITION_NAME = 'swagger_definition_name';
47
48
    private $legacyNormalizer;
49
50
    /**
51
     * @param ContainerInterface|FilterCollection|null $filterLocator The new filter locator or the deprecated filter collection
52
     */
53
    public function __construct(ResourceMetadataFactoryInterface $resourceMetadataFactory, PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, ResourceClassResolverInterface $resourceClassResolver, OperationMethodResolverInterface $operationMethodResolver, OperationPathResolverInterface $operationPathResolver, $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', NormalizerInterface $legacyNormalizer = null, array $defaultContext = [])
54
    {
55
        $this->setFilterLocator($filterLocator, true);
56
57
        $this->resourceMetadataFactory = $resourceMetadataFactory;
58
        $this->propertyNameCollectionFactory = $propertyNameCollectionFactory;
59
        $this->propertyMetadataFactory = $propertyMetadataFactory;
60
        $this->resourceClassResolver = $resourceClassResolver;
61
        $this->operationMethodResolver = $operationMethodResolver;
62
        $this->operationPathResolver = $operationPathResolver;
63
        $this->subresourceOperationFactory = $subresourceOperationFactory;
64
        $this->legacyNormalizer = $legacyNormalizer;
65
        $this->nameConverter = $nameConverter;
66
        $this->oauthEnabled = $oauthEnabled;
67
        $this->oauthType = $oauthType;
68
        $this->oauthFlow = $oauthFlow;
69
        $this->oauthTokenUrl = $oauthTokenUrl;
70
        $this->oauthAuthorizationUrl = $oauthAuthorizationUrl;
71
        $this->oauthScopes = $oauthScopes;
72
        $this->paginationEnabled = $paginationEnabled;
73
        $this->paginationPageParameterName = $paginationPageParameterName;
74
        $this->apiKeys = $apiKeys;
75
        $this->subresourceOperationFactory = $subresourceOperationFactory;
76
        $this->clientItemsPerPage = $clientItemsPerPage;
77
        $this->itemsPerPageParameterName = $itemsPerPageParameterName;
78
        $this->formatsProvider = $formatsProvider;
79
        $this->paginationClientEnabled = $paginationClientEnabled;
80
        $this->paginationClientEnabledParameterName = $paginationClientEnabledParameterName;
81
82
        $this->defaultContext = array_merge($this->defaultContext, $defaultContext);
83
    }
84
85
    /**
86
     * {@inheritdoc}
87
     */
88
    public function normalize($object, $format = null, array $context = [])
89
    {
90
        if (2 === ($context['spec_version'] ?? 3) || ($context['api_gateway'] ?? false)) {
91
            return $this->legacyNormalizer->normalize($object, $format, $context);
0 ignored issues
show
Bug introduced by
The method normalize() does not exist on null. ( Ignorable by Annotation )

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

91
            return $this->legacyNormalizer->/** @scrutinizer ignore-call */ normalize($object, $format, $context);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
92
        }
93
94
        $mimeTypes = $object->getMimeTypes();
95
        $definitions = new \ArrayObject();
96
        $paths = new \ArrayObject();
97
98
        foreach ($object->getResourceNameCollection() as $resourceClass) {
99
            $resourceMetadata = $this->resourceMetadataFactory->create($resourceClass);
100
            $resourceShortName = $resourceMetadata->getShortName();
101
102
            $this->addPaths($paths, $definitions, $resourceClass, $resourceShortName, $resourceMetadata, $mimeTypes, OperationType::COLLECTION);
103
            $this->addPaths($paths, $definitions, $resourceClass, $resourceShortName, $resourceMetadata, $mimeTypes, OperationType::ITEM);
104
105
            if (null === $this->subresourceOperationFactory) {
106
                continue;
107
            }
108
109
            foreach ($this->subresourceOperationFactory->create($resourceClass) as $operationId => $subresourceOperation) {
110
                $operationName = 'get';
111
                $subResourceMetadata = $this->resourceMetadataFactory->create($subresourceOperation['resource_class']);
112
                $serializerContext = $this->getSerializerContext(OperationType::SUBRESOURCE, false, $subResourceMetadata, $operationName);
113
                $responseDefinitionKey = $this->getDefinition($definitions, $subResourceMetadata, $subresourceOperation['resource_class'], $serializerContext);
114
115
                $pathOperation = new \ArrayObject([]);
116
                $pathOperation['tags'] = $subresourceOperation['shortNames'];
117
                $pathOperation['operationId'] = $operationId;
118
                if (null !== $this->formatsProvider) {
119
                    $responseFormats = $this->formatsProvider->getFormatsFromOperation($subresourceOperation['resource_class'], $operationName, OperationType::SUBRESOURCE);
120
                    $responseMimeTypes = $this->extractMimeTypes($responseFormats);
121
                }
122
                $pathOperation['summary'] = sprintf('Retrieves %s%s resource%s.', $subresourceOperation['collection'] ? 'the collection of ' : 'a ', $subresourceOperation['shortNames'][0], $subresourceOperation['collection'] ? 's' : '');
123
                $pathOperation['responses'] = [
124
                    '200' => $subresourceOperation['collection'] ? [
125
                        'description' => sprintf('%s collection response', $subresourceOperation['shortNames'][0]),
126
                        'content' => array_fill_keys($responseMimeTypes ?? $mimeTypes, ['schema' => ['type' => 'array', 'items' => ['$ref' => sprintf('#/components/schemas/%s', $responseDefinitionKey)]]]),
127
                    ] : [
128
                        'description' => sprintf('%s resource response', $subresourceOperation['shortNames'][0]),
129
                        'content' => array_fill_keys($responseMimeTypes ?? $mimeTypes, ['schema' => ['$ref' => sprintf('#/components/schemas/%s', $responseDefinitionKey)]]),
130
                    ],
131
                    '404' => ['description' => 'Resource not found'],
132
                ];
133
134
                // Avoid duplicates parameters when there is a filter on a subresource identifier
135
                $parametersMemory = [];
136
                $pathOperation['parameters'] = [];
137
138
                foreach ($subresourceOperation['identifiers'] as list($identifier, , $hasIdentifier)) {
139
                    if (true === $hasIdentifier) {
140
                        $pathOperation['parameters'][] = ['name' => $identifier, 'in' => 'path', 'required' => true, 'schema' => ['type' => 'string']];
141
                        $parametersMemory[] = $identifier;
142
                    }
143
                }
144
145
                if ($parameters = $this->getFiltersParameters($subresourceOperation['resource_class'], $operationName, $subResourceMetadata, $definitions, $serializerContext)) {
146
                    foreach ($parameters as $parameter) {
147
                        if (!\in_array($parameter['name'], $parametersMemory, true)) {
148
                            $pathOperation['parameters'][] = $parameter;
149
                        }
150
                    }
151
                }
152
153
                $paths[$this->getPath($subresourceOperation['shortNames'][0], $subresourceOperation['route_name'], $subresourceOperation, OperationType::SUBRESOURCE)] = new \ArrayObject(['get' => $pathOperation]);
154
            }
155
        }
156
157
        $definitions->ksort();
158
        $paths->ksort();
159
160
        return $this->computeDoc($object, $definitions, $paths, $context);
161
    }
162
163
    /**
164
     * @return \ArrayObject
165
     */
166
    protected function updateGetOperation(\ArrayObject $pathOperation, array $mimeTypes, string $operationType, ResourceMetadata $resourceMetadata, string $resourceClass, string $resourceShortName, string $operationName, \ArrayObject $definitions)
167
    {
168
        $serializerContext = $this->getSerializerContext($operationType, false, $resourceMetadata, $operationName);
169
        $responseDefinitionKey = $this->getDefinition($definitions, $resourceMetadata, $resourceClass, $serializerContext);
170
171
        if (OperationType::COLLECTION === $operationType) {
172
            $pathOperation['summary'] ?? $pathOperation['summary'] = sprintf('Retrieves the collection of %s resources.', $resourceShortName);
173
174
            $pathOperation['responses'] ?? $pathOperation['responses'] = [
175
                '200' => [
176
                    'description' => sprintf('%s collection response', $resourceShortName),
177
                    'content' => array_fill_keys($mimeTypes, [
178
                        'schema' => [
179
                            'type' => 'array',
180
                            'items' => ['$ref' => sprintf('#/components/schemas/%s', $responseDefinitionKey)],
181
                        ],
182
                    ]),
183
                ],
184
            ];
185
186
            $pathOperation['parameters'] ?? $pathOperation['parameters'] = $this->getFiltersParameters($resourceClass, $operationName, $resourceMetadata, $definitions, $serializerContext);
187
188
            if ($this->paginationEnabled && $resourceMetadata->getCollectionOperationAttribute($operationName, 'pagination_enabled', true, true)) {
189
                $pathOperation['parameters'][] = $this->getPaginationParameters();
190
191
                if ($resourceMetadata->getCollectionOperationAttribute($operationName, 'pagination_client_items_per_page', $this->clientItemsPerPage, true)) {
192
                    $pathOperation['parameters'][] = $this->getItemsPerPageParameters();
193
                }
194
            }
195
            if ($this->paginationEnabled && $resourceMetadata->getCollectionOperationAttribute($operationName, 'pagination_client_enabled', $this->paginationClientEnabled, true)) {
196
                $pathOperation['parameters'][] = $this->getPaginationClientEnabledParameters();
197
            }
198
199
            return $pathOperation;
200
        }
201
202
        $pathOperation['summary'] ?? $pathOperation['summary'] = sprintf('Retrieves a %s resource.', $resourceShortName);
203
        $pathOperation['parameters'] ?? $pathOperation['parameters'] = [[
204
            'name' => 'id',
205
            'in' => 'path',
206
            'required' => true,
207
            'schema' => [
208
                'type' => 'string',
209
            ],
210
        ]];
211
212
        $pathOperation['responses'] ?? $pathOperation['responses'] = [
213
            '200' => [
214
                'description' => sprintf('%s resource response', $resourceShortName),
215
                'content' => array_fill_keys($mimeTypes, [
216
                    'schema' => ['$ref' => sprintf('#/components/schemas/%s', $responseDefinitionKey)],
217
                ]),
218
            ],
219
            '404' => ['description' => 'Resource not found'],
220
        ];
221
222
        return $pathOperation;
223
    }
224
225
    /**
226
     * @return \ArrayObject
227
     */
228
    protected function updatePostOperation(\ArrayObject $pathOperation, array $mimeTypes, string $operationType, ResourceMetadata $resourceMetadata, string $resourceClass, string $resourceShortName, string $operationName, \ArrayObject $definitions)
229
    {
230
        $pathOperation['summary'] ?? $pathOperation['summary'] = sprintf('Creates a %s resource.', $resourceShortName);
231
        $pathOperation['responses'] ?? $pathOperation['responses'] = [
232
            '201' => [
233
                'description' => sprintf('%s resource created', $resourceShortName),
234
                'content' => array_fill_keys($mimeTypes, [
235
                    'schema' => ['$ref' => sprintf('#/components/schemas/%s', $this->getDefinition($definitions, $resourceMetadata, $resourceClass,
236
                        $this->getSerializerContext($operationType, false, $resourceMetadata, $operationName))),
237
                    ],
238
                ]),
239
            ],
240
            '400' => ['description' => 'Invalid input'],
241
            '404' => ['description' => 'Resource not found'],
242
        ];
243
244
        $pathOperation['requestBody'] ?? $pathOperation['requestBody'] = [
245
            'content' => array_fill_keys($mimeTypes, [
246
                'schema' => ['$ref' => sprintf('#/components/schemas/%s', $this->getDefinition($definitions, $resourceMetadata, $resourceClass,
247
                    $this->getSerializerContext($operationType, true, $resourceMetadata, $operationName))),
248
                ],
249
            ]),
250
            'description' => sprintf('The new %s resource', $resourceShortName),
251
        ];
252
253
        return $pathOperation;
254
    }
255
256
    /**
257
     * @return \ArrayObject
258
     */
259
    protected function updatePutOperation(\ArrayObject $pathOperation, array $mimeTypes, string $operationType, ResourceMetadata $resourceMetadata, string $resourceClass, string $resourceShortName, string $operationName, \ArrayObject $definitions)
260
    {
261
        $pathOperation['summary'] ?? $pathOperation['summary'] = sprintf('Replaces the %s resource.', $resourceShortName);
262
        $pathOperation['parameters'] ?? $pathOperation['parameters'] = [
263
            [
264
                'name' => 'id',
265
                'in' => 'path',
266
                'required' => true,
267
                'schema' => [
268
                    'type' => 'string',
269
                ],
270
            ],
271
        ];
272
273
        $pathOperation['responses'] ?? $pathOperation['responses'] = [
274
            '200' => [
275
                'description' => sprintf('%s resource updated', $resourceShortName),
276
                'content' => array_fill_keys($mimeTypes, [
277
                    'schema' => ['$ref' => sprintf('#/components/schemas/%s', $this->getDefinition($definitions, $resourceMetadata, $resourceClass,
278
                        $this->getSerializerContext($operationType, false, $resourceMetadata, $operationName))),
279
                    ],
280
                ]),
281
            ],
282
            '400' => ['description' => 'Invalid input'],
283
            '404' => ['description' => 'Resource not found'],
284
        ];
285
286
        $pathOperation['requestBody'] ?? $pathOperation['requestBody'] = [
287
            'content' => array_fill_keys($mimeTypes, [
288
                'schema' => ['$ref' => sprintf('#/components/schemas/%s', $this->getDefinition($definitions, $resourceMetadata, $resourceClass,
289
                    $this->getSerializerContext($operationType, true, $resourceMetadata, $operationName))),
290
                ],
291
            ]),
292
            'description' => sprintf('The updated %s resource', $resourceShortName),
293
        ];
294
295
        return $pathOperation;
296
    }
297
298
    protected function updateDeleteOperation(\ArrayObject $pathOperation, string $resourceShortName): \ArrayObject
299
    {
300
        $pathOperation['summary'] ?? $pathOperation['summary'] = sprintf('Removes the %s resource.', $resourceShortName);
301
        $pathOperation['responses'] ?? $pathOperation['responses'] = [
302
            '204' => ['description' => sprintf('%s resource deleted', $resourceShortName)],
303
            '404' => ['description' => 'Resource not found'],
304
        ];
305
306
        $pathOperation['parameters'] ?? $pathOperation['parameters'] = [[
307
            'name' => 'id',
308
            'in' => 'path',
309
            'required' => true,
310
            'schema' => [
311
                'type' => 'string',
312
            ],
313
        ]];
314
315
        return $pathOperation;
316
    }
317
318
    /**
319
     * Computes the OpenAPI documentation.
320
     */
321
    private function computeDoc(Documentation $documentation, \ArrayObject $definitions, \ArrayObject $paths, array $context): array
322
    {
323
        $doc = [
324
            'openapi' => self::OPENAPI_VERSION,
325
            'info' => [
326
                'title' => $documentation->getTitle(),
327
                'version' => $documentation->getVersion(),
328
            ],
329
            'paths' => $paths,
330
            'servers' => [
331
                ['url' => $context['base_url'] ?? '/'],
332
            ],
333
        ];
334
335
        if ('' !== $description = $documentation->getDescription()) {
336
            $doc['info']['description'] = $description;
337
        }
338
339
        $securityDefinitions = [];
340
        $security = [];
341
342
        if ($this->oauthEnabled) {
343
            $securityDefinitions['oauth'] = [
344
                'type' => $this->oauthType,
345
                'description' => 'OAuth client_credentials Grant',
346
                'flow' => $this->oauthFlow,
347
                'tokenUrl' => $this->oauthTokenUrl,
348
                'authorizationUrl' => $this->oauthAuthorizationUrl,
349
                'scopes' => $this->oauthScopes,
350
            ];
351
352
            $security[] = ['oauth' => []];
353
        }
354
355
        foreach ($this->apiKeys as $key => $apiKey) {
356
            $name = $apiKey['name'];
357
            $type = $apiKey['type'];
358
359
            $securityDefinitions[$key] = [
360
                'type' => 'apiKey',
361
                'in' => $type,
362
                'description' => sprintf('Value for the %s %s', $name, 'query' === $type ? sprintf('%s parameter', $type) : $type),
363
                'name' => $name,
364
            ];
365
366
            $security[] = [$key => []];
367
        }
368
369
        if ($securityDefinitions && $security) {
370
            $doc['security'] = $security;
371
        }
372
373
        if (\count($definitions) + \count($securityDefinitions)) {
374
            $doc['components'] = [];
375
            $doc['components']['schemas'] = \count($definitions) ? $definitions : null;
376
            $doc['components']['securitySchemes'] = \count($securityDefinitions) ? $securityDefinitions : null;
377
            $doc['components'] = array_filter($doc['components']);
378
        }
379
380
        return $doc;
381
    }
382
383
    /**
384
     * Gets OpenAPI parameters corresponding to enabled filters.
385
     */
386
    private function getFiltersParameters(string $resourceClass, string $operationName, ResourceMetadata $resourceMetadata, \ArrayObject $definitions, array $serializerContext = null): array
387
    {
388
        if (null === $this->filterLocator) {
389
            return [];
390
        }
391
392
        $parameters = [];
393
        $resourceFilters = $resourceMetadata->getCollectionOperationAttribute($operationName, 'filters', [], true);
394
        foreach ($resourceFilters as $filterId) {
395
            if (!$filter = $this->getFilter($filterId)) {
396
                continue;
397
            }
398
399
            foreach ($filter->getDescription($resourceClass) as $name => $data) {
400
                $parameter = [
401
                    'name' => $name,
402
                    'in' => 'query',
403
                    'required' => $data['required'],
404
                    'schema' => $this->getType($data['type'], $data['is_collection'] ?? false, null, null, $definitions, $serializerContext),
405
                ];
406
407
                if ('array' === $parameter['schema']['type'] ?? '') {
408
                    $parameter['style'] = \in_array($data['type'], [Type::BUILTIN_TYPE_ARRAY, Type::BUILTIN_TYPE_OBJECT], true) ? 'deepObject' : 'form';
409
                    $parameter['explode'] = true;
410
                }
411
412
                if (isset($data['openapi'])) {
413
                    $parameter = $data['openapi'] + $parameter;
414
                }
415
416
                $parameters[] = $parameter;
417
            }
418
        }
419
420
        return $parameters;
421
    }
422
423
    /**
424
     * {@inheritdoc}
425
     */
426
    protected function getPaginationParameters(): array
427
    {
428
        $parameters = array_merge(parent::getPaginationParameters(), ['schema' => ['type' => 'integer']]);
429
        unset($parameters['type']);
430
431
        return $parameters;
432
    }
433
434
    /**
435
     * {@inheritdoc}
436
     */
437
    protected function getPaginationClientEnabledParameters(): array
438
    {
439
        $parameters = array_merge(parent::getPaginationClientEnabledParameters(), ['schema' => ['type' => 'boolean']]);
440
        unset($parameters['type']);
441
442
        return $parameters;
443
    }
444
445
    /**
446
     * {@inheritdoc}
447
     */
448
    protected function getItemsPerPageParameters(): array
449
    {
450
        $parameters = array_merge(parent::getItemsPerPageParameters(), ['schema' => ['type' => 'integer']]);
451
        unset($parameters['type']);
452
453
        return $parameters;
454
    }
455
456
    /**
457
     * {@inheritdoc}
458
     */
459
    protected function getType(string $type, bool $isCollection, string $className = null, bool $readableLink = null, \ArrayObject $definitions, array $serializerContext = null): array
460
    {
461
        $type = parent::getType($type, $isCollection, $className, $readableLink, $definitions, $serializerContext);
462
463
        if ($type['$ref'] ?? false) {
464
            $type['$ref'] = str_replace('#/definitions/', '#/components/schemas/', $type['$ref']);
465
        }
466
467
        return $type;
468
    }
469
}
470