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

DocumentationNormalizer::computeDoc()   B

Complexity

Conditions 10
Paths 80

Size

Total Lines 60
Code Lines 38

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 38
dl 0
loc 60
rs 7.6666
c 0
b 0
f 0
cc 10
nc 80
nop 4

How to fix   Long Method    Complexity   

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:

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