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

DocumentationNormalizer::getPathOperation()   B

Complexity

Conditions 8
Paths 32

Size

Total Lines 28
Code Lines 21

Duplication

Lines 0
Ratio 0 %

Importance

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

How to fix   Many Parameters   

Many Parameters

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

There are several approaches to avoid long parameter lists:

1
<?php
2
3
/*
4
 * This file is part of the API Platform project.
5
 *
6
 * (c) Kévin Dunglas <[email protected]>
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 */
11
12
declare(strict_types=1);
13
14
namespace ApiPlatform\Core\Swagger\Serializer;
15
16
use ApiPlatform\Core\Api\FilterCollection;
17
use ApiPlatform\Core\Api\FilterLocatorTrait;
18
use ApiPlatform\Core\Api\OperationAwareFormatsProviderInterface;
19
use ApiPlatform\Core\Api\OperationMethodResolverInterface;
20
use ApiPlatform\Core\Api\OperationType;
21
use ApiPlatform\Core\Api\ResourceClassResolverInterface;
22
use ApiPlatform\Core\Api\UrlGeneratorInterface;
23
use ApiPlatform\Core\Documentation\Documentation;
24
use ApiPlatform\Core\Metadata\Property\Factory\PropertyMetadataFactoryInterface;
25
use ApiPlatform\Core\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface;
26
use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface;
27
use ApiPlatform\Core\Metadata\Resource\ResourceMetadata;
28
use ApiPlatform\Core\OpenApi\Serializer\AbstractDocumentationNormalizer;
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
35
/**
36
 * Creates a machine readable Swagger API documentation.
37
 *
38
 * @author Amrouche Hamza <[email protected]>
39
 * @author Teoh Han Hui <[email protected]>
40
 * @author Kévin Dunglas <[email protected]>
41
 */
42
final class DocumentationNormalizer extends AbstractDocumentationNormalizer
43
{
44
    use FilterLocatorTrait;
45
46
    const SWAGGER_VERSION = '2.0';
47
    const SWAGGER_DEFINITION_NAME = 'swagger_definition_name';
48
    const ATTRIBUTE_NAME = 'swagger_context';
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, 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 = [])
54
    {
55
        if ($urlGenerator) {
56
            @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);
57
        }
58
59
        $this->setFilterLocator($filterLocator, true);
60
61
        $this->resourceMetadataFactory = $resourceMetadataFactory;
62
        $this->propertyNameCollectionFactory = $propertyNameCollectionFactory;
63
        $this->propertyMetadataFactory = $propertyMetadataFactory;
64
        $this->resourceClassResolver = $resourceClassResolver;
65
        $this->operationMethodResolver = $operationMethodResolver;
66
        $this->operationPathResolver = $operationPathResolver;
67
        $this->nameConverter = $nameConverter;
68
        $this->oauthEnabled = $oauthEnabled;
69
        $this->oauthType = $oauthType;
70
        $this->oauthFlow = $oauthFlow;
71
        $this->oauthTokenUrl = $oauthTokenUrl;
72
        $this->oauthAuthorizationUrl = $oauthAuthorizationUrl;
73
        $this->oauthScopes = $oauthScopes;
74
        $this->subresourceOperationFactory = $subresourceOperationFactory;
75
        $this->paginationEnabled = $paginationEnabled;
76
        $this->paginationPageParameterName = $paginationPageParameterName;
77
        $this->apiKeys = $apiKeys;
78
        $this->subresourceOperationFactory = $subresourceOperationFactory;
79
        $this->clientItemsPerPage = $clientItemsPerPage;
80
        $this->itemsPerPageParameterName = $itemsPerPageParameterName;
81
        $this->formatsProvider = $formatsProvider;
82
        $this->paginationClientEnabled = $paginationClientEnabled;
83
        $this->paginationClientEnabledParameterName = $paginationClientEnabledParameterName;
84
85
        $this->defaultContext = array_merge($this->defaultContext, $defaultContext);
86
    }
87
88
    /**
89
     * {@inheritdoc}
90
     */
91
    public function normalize($object, $format = null, array $context = [])
92
    {
93
        $mimeTypes = $object->getMimeTypes();
94
        $definitions = new \ArrayObject();
95
        $paths = new \ArrayObject();
96
97
        foreach ($object->getResourceNameCollection() as $resourceClass) {
98
            $resourceMetadata = $this->resourceMetadataFactory->create($resourceClass);
99
            $resourceShortName = $resourceMetadata->getShortName();
100
101
            $this->addPaths($paths, $definitions, $resourceClass, $resourceShortName, $resourceMetadata, $mimeTypes, OperationType::COLLECTION);
102
            $this->addPaths($paths, $definitions, $resourceClass, $resourceShortName, $resourceMetadata, $mimeTypes, OperationType::ITEM);
103
104
            if (null === $this->subresourceOperationFactory) {
105
                continue;
106
            }
107
108
            foreach ($this->subresourceOperationFactory->create($resourceClass) as $operationId => $subresourceOperation) {
109
                $operationName = 'get';
110
                $subResourceMetadata = $this->resourceMetadataFactory->create($subresourceOperation['resource_class']);
111
                $serializerContext = $this->getSerializerContext(OperationType::SUBRESOURCE, false, $subResourceMetadata, $operationName);
112
                $responseDefinitionKey = $this->getDefinition($definitions, $subResourceMetadata, $subresourceOperation['resource_class'], $serializerContext);
113
114
                $pathOperation = new \ArrayObject([]);
115
                $pathOperation['tags'] = $subresourceOperation['shortNames'];
116
                $pathOperation['operationId'] = $operationId;
117
                if (null !== $this->formatsProvider) {
118
                    $responseFormats = $this->formatsProvider->getFormatsFromOperation($subresourceOperation['resource_class'], $operationName, OperationType::SUBRESOURCE);
119
                    $responseMimeTypes = $this->extractMimeTypes($responseFormats);
120
                }
121
                $pathOperation['produces'] = $responseMimeTypes ?? $mimeTypes;
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
                        'schema' => ['type' => 'array', 'items' => ['$ref' => sprintf('#/definitions/%s', $responseDefinitionKey)]],
127
                    ] : [
128
                        'description' => sprintf('%s resource response', $subresourceOperation['shortNames'][0]),
129
                        'schema' => ['$ref' => sprintf('#/definitions/%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, '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
        $pathOperation['produces'] ?? $pathOperation['produces'] = $mimeTypes;
172
173
        if (OperationType::COLLECTION === $operationType) {
174
            $pathOperation['summary'] ?? $pathOperation['summary'] = sprintf('Retrieves the collection of %s resources.', $resourceShortName);
175
            $pathOperation['responses'] ?? $pathOperation['responses'] = [
176
                '200' => [
177
                    'description' => sprintf('%s collection response', $resourceShortName),
178
                    'schema' => [
179
                        'type' => 'array',
180
                        'items' => ['$ref' => sprintf('#/definitions/%s', $responseDefinitionKey)],
181
                    ],
182
                ],
183
            ];
184
            $pathOperation['parameters'] ?? $pathOperation['parameters'] = $this->getFiltersParameters($resourceClass, $operationName, $resourceMetadata, $definitions, $serializerContext);
185
186
            if ($this->paginationEnabled && $resourceMetadata->getCollectionOperationAttribute($operationName, 'pagination_enabled', true, true)) {
187
                $pathOperation['parameters'][] = $this->getPaginationParameters();
188
189
                if ($resourceMetadata->getCollectionOperationAttribute($operationName, 'pagination_client_items_per_page', $this->clientItemsPerPage, true)) {
190
                    $pathOperation['parameters'][] = $this->getItemsPerPageParameters();
191
                }
192
            }
193
            if ($this->paginationEnabled && $resourceMetadata->getCollectionOperationAttribute($operationName, 'pagination_client_enabled', $this->paginationClientEnabled, true)) {
194
                $pathOperation['parameters'][] = $this->getPaginationClientEnabledParameters();
195
            }
196
197
            return $pathOperation;
198
        }
199
200
        $pathOperation['summary'] ?? $pathOperation['summary'] = sprintf('Retrieves a %s resource.', $resourceShortName);
201
        $pathOperation['parameters'] ?? $pathOperation['parameters'] = [[
202
            'name' => 'id',
203
            'in' => 'path',
204
            'required' => true,
205
            'type' => 'string',
206
        ]];
207
        $pathOperation['responses'] ?? $pathOperation['responses'] = [
208
            '200' => [
209
                'description' => sprintf('%s resource response', $resourceShortName),
210
                'schema' => ['$ref' => sprintf('#/definitions/%s', $responseDefinitionKey)],
211
            ],
212
            '404' => ['description' => 'Resource not found'],
213
        ];
214
215
        return $pathOperation;
216
    }
217
218
    /**
219
     * @return \ArrayObject
220
     */
221
    protected function updatePostOperation(\ArrayObject $pathOperation, array $mimeTypes, string $operationType, ResourceMetadata $resourceMetadata, string $resourceClass, string $resourceShortName, string $operationName, \ArrayObject $definitions)
222
    {
223
        $pathOperation['consumes'] ?? $pathOperation['consumes'] = $mimeTypes;
224
        $pathOperation['produces'] ?? $pathOperation['produces'] = $mimeTypes;
225
        $pathOperation['summary'] ?? $pathOperation['summary'] = sprintf('Creates a %s resource.', $resourceShortName);
226
        $pathOperation['parameters'] ?? $pathOperation['parameters'] = [[
227
            'name' => lcfirst($resourceShortName),
228
            'in' => 'body',
229
            'description' => sprintf('The new %s resource', $resourceShortName),
230
            'schema' => ['$ref' => sprintf('#/definitions/%s', $this->getDefinition($definitions, $resourceMetadata, $resourceClass,
231
                $this->getSerializerContext($operationType, true, $resourceMetadata, $operationName)
232
            ))],
233
        ]];
234
        $pathOperation['responses'] ?? $pathOperation['responses'] = [
235
            '201' => [
236
                'description' => sprintf('%s resource created', $resourceShortName),
237
                'schema' => ['$ref' => sprintf('#/definitions/%s', $this->getDefinition($definitions, $resourceMetadata, $resourceClass,
238
                    $this->getSerializerContext($operationType, false, $resourceMetadata, $operationName)
239
                ))],
240
            ],
241
            '400' => ['description' => 'Invalid input'],
242
            '404' => ['description' => 'Resource not found'],
243
        ];
244
245
        return $pathOperation;
246
    }
247
248
    /**
249
     * @return \ArrayObject
250
     */
251
    protected function updatePutOperation(\ArrayObject $pathOperation, array $mimeTypes, string $operationType, ResourceMetadata $resourceMetadata, string $resourceClass, string $resourceShortName, string $operationName, \ArrayObject $definitions)
252
    {
253
        $pathOperation['consumes'] ?? $pathOperation['consumes'] = $mimeTypes;
254
        $pathOperation['produces'] ?? $pathOperation['produces'] = $mimeTypes;
255
        $pathOperation['summary'] ?? $pathOperation['summary'] = sprintf('Replaces the %s resource.', $resourceShortName);
256
        $pathOperation['parameters'] ?? $pathOperation['parameters'] = [
257
            [
258
                'name' => 'id',
259
                'in' => 'path',
260
                'type' => 'string',
261
                'required' => true,
262
            ],
263
            [
264
                'name' => lcfirst($resourceShortName),
265
                'in' => 'body',
266
                'description' => sprintf('The updated %s resource', $resourceShortName),
267
                'schema' => ['$ref' => sprintf('#/definitions/%s', $this->getDefinition($definitions, $resourceMetadata, $resourceClass,
268
                    $this->getSerializerContext($operationType, true, $resourceMetadata, $operationName)
269
                ))],
270
            ],
271
        ];
272
        $pathOperation['responses'] ?? $pathOperation['responses'] = [
273
            '200' => [
274
                'description' => sprintf('%s resource updated', $resourceShortName),
275
                'schema' => ['$ref' => sprintf('#/definitions/%s', $this->getDefinition($definitions, $resourceMetadata, $resourceClass,
276
                    $this->getSerializerContext($operationType, false, $resourceMetadata, $operationName)
277
                ))],
278
            ],
279
            '400' => ['description' => 'Invalid input'],
280
            '404' => ['description' => 'Resource not found'],
281
        ];
282
283
        return $pathOperation;
284
    }
285
286
    protected function updateDeleteOperation(\ArrayObject $pathOperation, string $resourceShortName): \ArrayObject
287
    {
288
        $pathOperation['summary'] ?? $pathOperation['summary'] = sprintf('Removes the %s resource.', $resourceShortName);
289
        $pathOperation['responses'] ?? $pathOperation['responses'] = [
290
            '204' => ['description' => sprintf('%s resource deleted', $resourceShortName)],
291
            '404' => ['description' => 'Resource not found'],
292
        ];
293
294
        $pathOperation['parameters'] ?? $pathOperation['parameters'] = [[
295
            'name' => 'id',
296
            'in' => 'path',
297
            'type' => 'string',
298
            'required' => true,
299
        ]];
300
301
        return $pathOperation;
302
    }
303
304
    /**
305
     * Computes the Swagger documentation.
306
     */
307
    protected function computeDoc(Documentation $documentation, \ArrayObject $definitions, \ArrayObject $paths, array $context): array
308
    {
309
        $doc = [
310
            'swagger' => self::SWAGGER_VERSION,
311
            'basePath' => $context[self::BASE_URL] ?? $this->defaultContext[self::BASE_URL],
312
            'info' => [
313
                'title' => $documentation->getTitle(),
314
                'version' => $documentation->getVersion(),
315
            ],
316
            'paths' => $paths,
317
        ];
318
319
        $securityDefinitions = [];
320
        $security = [];
321
322
        if ($this->oauthEnabled) {
323
            $securityDefinitions['oauth'] = [
324
                'type' => $this->oauthType,
325
                'description' => 'OAuth client_credentials Grant',
326
                'flow' => $this->oauthFlow,
327
                'tokenUrl' => $this->oauthTokenUrl,
328
                'authorizationUrl' => $this->oauthAuthorizationUrl,
329
                'scopes' => $this->oauthScopes,
330
            ];
331
332
            $security[] = ['oauth' => []];
333
        }
334
335
        if ($this->apiKeys) {
336
            foreach ($this->apiKeys as $key => $apiKey) {
337
                $name = $apiKey['name'];
338
                $type = $apiKey['type'];
339
340
                $securityDefinitions[$key] = [
341
                    'type' => 'apiKey',
342
                    'in' => $type,
343
                    'description' => sprintf('Value for the %s %s', $name, 'query' === $type ? sprintf('%s parameter', $type) : $type),
344
                    'name' => $name,
345
                ];
346
347
                $security[] = [$key => []];
348
            }
349
        }
350
351
        if ($securityDefinitions && $security) {
352
            $doc['securityDefinitions'] = $securityDefinitions;
353
            $doc['security'] = $security;
354
        }
355
356
        if ('' !== $description = $documentation->getDescription()) {
357
            $doc['info']['description'] = $description;
358
        }
359
360
        if (\count($definitions) > 0) {
361
            $doc['definitions'] = $definitions;
362
        }
363
364
        return $doc;
365
    }
366
367
    /**
368
     * Gets Swagger parameters corresponding to enabled filters.
369
     */
370
    private function getFiltersParameters(string $resourceClass, string $operationName, ResourceMetadata $resourceMetadata, \ArrayObject $definitions, array $serializerContext = null): array
371
    {
372
        if (null === $this->filterLocator) {
373
            return [];
374
        }
375
376
        $parameters = [];
377
        $resourceFilters = $resourceMetadata->getCollectionOperationAttribute($operationName, 'filters', [], true);
378
        foreach ($resourceFilters as $filterId) {
379
            if (!$filter = $this->getFilter($filterId)) {
380
                continue;
381
            }
382
383
            foreach ($filter->getDescription($resourceClass) as $name => $data) {
384
                $parameter = [
385
                    'name' => $name,
386
                    'in' => 'query',
387
                    'required' => $data['required'],
388
                ];
389
                $parameter += $this->getType($data['type'], $data['is_collection'] ?? false, null, null, $definitions, $serializerContext);
390
391
                if ('array' === $parameter['type']) {
392
                    $parameter['collectionFormat'] = \in_array($data['type'], [Type::BUILTIN_TYPE_ARRAY, Type::BUILTIN_TYPE_OBJECT], true) ? 'csv' : 'multi';
393
                }
394
395
                if (isset($data['swagger'])) {
396
                    $parameter = $data['swagger'] + $parameter;
397
                }
398
399
                $parameters[] = $parameter;
400
            }
401
        }
402
403
        return $parameters;
404
    }
405
}
406