Completed
Push — master ( 9c9f2f...4d39fe )
by Antoine
02:39 queued 02:31
created

DocumentationNormalizer::updateGetOperation()   C

Complexity

Conditions 7
Paths 7

Size

Total Lines 51
Code Lines 31

Duplication

Lines 3
Ratio 5.88 %

Importance

Changes 0
Metric Value
dl 3
loc 51
rs 6.9743
c 0
b 0
f 0
cc 7
eloc 31
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\Swagger\Serializer;
15
16
use ApiPlatform\Core\Api\FilterCollection;
17
use ApiPlatform\Core\Api\FilterLocatorTrait;
18
use ApiPlatform\Core\Api\OperationMethodResolverInterface;
19
use ApiPlatform\Core\Api\OperationType;
20
use ApiPlatform\Core\Api\ResourceClassResolverInterface;
21
use ApiPlatform\Core\Api\UrlGeneratorInterface;
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\Property\PropertyMetadata;
26
use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface;
27
use ApiPlatform\Core\Metadata\Resource\ResourceMetadata;
28
use ApiPlatform\Core\Operation\Factory\SubresourceOperationFactoryInterface;
29
use ApiPlatform\Core\PathResolver\OperationPathResolverInterface;
30
use Psr\Container\ContainerInterface;
31
use Symfony\Component\PropertyInfo\Type;
32
use Symfony\Component\Serializer\NameConverter\NameConverterInterface;
33
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
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 implements NormalizerInterface
43
{
44
    use FilterLocatorTrait;
45
46
    const SWAGGER_VERSION = '2.0';
47
    const FORMAT = 'json';
48
49
    private $resourceMetadataFactory;
50
    private $propertyNameCollectionFactory;
51
    private $propertyMetadataFactory;
52
    private $resourceClassResolver;
53
    private $operationMethodResolver;
54
    private $operationPathResolver;
55
    private $nameConverter;
56
    private $oauthEnabled;
57
    private $oauthType;
58
    private $oauthFlow;
59
    private $oauthTokenUrl;
60
    private $oauthAuthorizationUrl;
61
    private $oauthScopes;
62
    private $subresourceOperationFactory;
63
    private $paginationEnabled;
64
    private $paginationPageParameterName;
65
    private $clientItemsPerPage;
66
    private $itemsPerPageParameterName;
67
68
    /**
69
     * @param ContainerInterface|FilterCollection|null $filterLocator The new filter locator or the deprecated filter collection
70
     */
71
    public function __construct(ResourceMetadataFactoryInterface $resourceMetadataFactory, PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, ResourceClassResolverInterface $resourceClassResolver, OperationMethodResolverInterface $operationMethodResolver, OperationPathResolverInterface $operationPathResolver, UrlGeneratorInterface $urlGenerator = null, $filterLocator = null, NameConverterInterface $nameConverter = null, $oauthEnabled = false, $oauthType = '', $oauthFlow = '', $oauthTokenUrl = '', $oauthAuthorizationUrl = '', $oauthScopes = [], SubresourceOperationFactoryInterface $subresourceOperationFactory = null, $paginationEnabled = true, $paginationPageParameterName = 'page', $clientItemsPerPage = false, $itemsPerPageParameterName = 'itemsPerPage')
72
    {
73
        if ($urlGenerator) {
74
            @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);
1 ignored issue
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
75
        }
76
77
        $this->setFilterLocator($filterLocator, true);
78
79
        $this->resourceMetadataFactory = $resourceMetadataFactory;
80
        $this->propertyNameCollectionFactory = $propertyNameCollectionFactory;
81
        $this->propertyMetadataFactory = $propertyMetadataFactory;
82
        $this->resourceClassResolver = $resourceClassResolver;
83
        $this->operationMethodResolver = $operationMethodResolver;
84
        $this->operationPathResolver = $operationPathResolver;
85
        $this->nameConverter = $nameConverter;
86
        $this->oauthEnabled = $oauthEnabled;
87
        $this->oauthType = $oauthType;
88
        $this->oauthFlow = $oauthFlow;
89
        $this->oauthTokenUrl = $oauthTokenUrl;
90
        $this->oauthAuthorizationUrl = $oauthAuthorizationUrl;
91
        $this->oauthScopes = $oauthScopes;
92
        $this->subresourceOperationFactory = $subresourceOperationFactory;
93
        $this->paginationEnabled = $paginationEnabled;
94
        $this->paginationPageParameterName = $paginationPageParameterName;
95
        $this->clientItemsPerPage = $clientItemsPerPage;
96
        $this->itemsPerPageParameterName = $itemsPerPageParameterName;
97
    }
98
99
    /**
100
     * {@inheritdoc}
101
     */
102
    public function normalize($object, $format = null, array $context = [])
103
    {
104
        $mimeTypes = $object->getMimeTypes();
105
        $definitions = new \ArrayObject();
106
        $paths = new \ArrayObject();
107
108
        foreach ($object->getResourceNameCollection() as $resourceClass) {
109
            $resourceMetadata = $this->resourceMetadataFactory->create($resourceClass);
110
            $resourceShortName = $resourceMetadata->getShortName();
111
112
            $this->addPaths($paths, $definitions, $resourceClass, $resourceShortName, $resourceMetadata, $mimeTypes, OperationType::COLLECTION);
113
            $this->addPaths($paths, $definitions, $resourceClass, $resourceShortName, $resourceMetadata, $mimeTypes, OperationType::ITEM);
114
115
            if (null === $this->subresourceOperationFactory) {
116
                continue;
117
            }
118
119
            foreach ($this->subresourceOperationFactory->create($resourceClass) as $operationId => $subresourceOperation) {
120
                $operationName = 'get';
121
                $serializerContext = $this->getSerializerContext(OperationType::SUBRESOURCE, false, $resourceMetadata, $operationName);
122
                $responseDefinitionKey = $this->getDefinition($definitions, $this->resourceMetadataFactory->create($subresourceOperation['resource_class']), $subresourceOperation['resource_class'], $serializerContext);
123
124
                $pathOperation = new \ArrayObject([]);
125
                $pathOperation['tags'] = $subresourceOperation['shortNames'];
126
                $pathOperation['operationId'] = $operationId;
127
                $pathOperation['produces'] = $mimeTypes;
128
                $pathOperation['summary'] = sprintf('Retrieves %s%s resource%s.', $subresourceOperation['collection'] ? 'the collection of ' : 'a ', $subresourceOperation['shortNames'][0], $subresourceOperation['collection'] ? 's' : '');
129
                $pathOperation['responses'] = [
130
                    '200' => $subresourceOperation['collection'] ? [
131
                        'description' => sprintf('%s collection response', $subresourceOperation['shortNames'][0]),
132
                        'schema' => ['type' => 'array', 'items' => ['$ref' => sprintf('#/definitions/%s', $responseDefinitionKey)]],
133
                    ] : [
134
                        'description' => sprintf('%s resource response', $subresourceOperation['shortNames'][0]),
135
                        'schema' => ['$ref' => sprintf('#/definitions/%s', $responseDefinitionKey)],
136
                    ],
137
                    '404' => ['description' => 'Resource not found'],
138
                ];
139
140
                // Avoid duplicates parameters when there is a filter on a subresource identifier
141
                $parametersMemory = [];
142
                $pathOperation['parameters'] = [];
143
144
                foreach ($subresourceOperation['identifiers'] as list($identifier, , $hasIdentifier)) {
145
                    if (true === $hasIdentifier) {
146
                        $pathOperation['parameters'][] = ['name' => $identifier, 'in' => 'path', 'required' => true, 'type' => 'string'];
147
                        $parametersMemory[] = $identifier;
148
                    }
149
                }
150
151
                if ($parameters = $this->getFiltersParameters($resourceClass, $operationName, $resourceMetadata, $definitions, $serializerContext)) {
152
                    foreach ($parameters as $parameter) {
153
                        if (!in_array($parameter['name'], $parametersMemory, true)) {
154
                            $pathOperation['parameters'][] = $parameter;
155
                        }
156
                    }
157
                }
158
159
                $paths[$this->getPath($subresourceOperation['shortNames'][0], $subresourceOperation['route_name'], $subresourceOperation, OperationType::SUBRESOURCE)] = new \ArrayObject(['get' => $pathOperation]);
160
            }
161
        }
162
163
        $definitions->ksort();
164
        $paths->ksort();
165
166
        return $this->computeDoc($object, $definitions, $paths, $context);
167
    }
168
169
    /**
170
     * Updates the list of entries in the paths collection.
171
     *
172
     * @param \ArrayObject     $paths
173
     * @param \ArrayObject     $definitions
174
     * @param string           $resourceClass
175
     * @param string           $resourceShortName
176
     * @param ResourceMetadata $resourceMetadata
177
     * @param array            $mimeTypes
178
     * @param string           $operationType
179
     */
180
    private function addPaths(\ArrayObject $paths, \ArrayObject $definitions, string $resourceClass, string $resourceShortName, ResourceMetadata $resourceMetadata, array $mimeTypes, string $operationType)
181
    {
182
        if (null === $operations = $operationType === OperationType::COLLECTION ? $resourceMetadata->getCollectionOperations() : $resourceMetadata->getItemOperations()) {
183
            return;
184
        }
185
186
        foreach ($operations as $operationName => $operation) {
187
            $path = $this->getPath($resourceShortName, $operationName, $operation, $operationType);
188
            $method = $operationType === OperationType::ITEM ? $this->operationMethodResolver->getItemOperationMethod($resourceClass, $operationName) : $this->operationMethodResolver->getCollectionOperationMethod($resourceClass, $operationName);
189
190
            $paths[$path][strtolower($method)] = $this->getPathOperation($operationName, $operation, $method, $operationType, $resourceClass, $resourceMetadata, $mimeTypes, $definitions);
191
        }
192
    }
193
194
    /**
195
     * Gets the path for an operation.
196
     *
197
     * If the path ends with the optional _format parameter, it is removed
198
     * as optional path parameters are not yet supported.
199
     *
200
     * @see https://github.com/OAI/OpenAPI-Specification/issues/93
201
     *
202
     * @param string $resourceShortName
203
     * @param string $operationName
204
     * @param array  $operation
205
     * @param string $operationType
206
     *
207
     * @return string
208
     */
209
    private function getPath(string $resourceShortName, string $operationName, array $operation, string $operationType): string
210
    {
211
        $path = $this->operationPathResolver->resolveOperationPath($resourceShortName, $operation, $operationType, $operationName);
0 ignored issues
show
Unused Code introduced by
The call to OperationPathResolverInt...:resolveOperationPath() has too many arguments starting with $operationName.

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.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
212
        if ('.{_format}' === substr($path, -10)) {
213
            $path = substr($path, 0, -10);
214
        }
215
216
        return $path;
217
    }
218
219
    /**
220
     * Gets a path Operation Object.
221
     *
222
     * @see https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#operation-object
223
     *
224
     * @param string           $operationName
225
     * @param array            $operation
226
     * @param string           $method
227
     * @param string           $operationType
228
     * @param string           $resourceClass
229
     * @param ResourceMetadata $resourceMetadata
230
     * @param string[]         $mimeTypes
231
     * @param \ArrayObject     $definitions
232
     *
233
     * @return \ArrayObject
234
     */
235
    private function getPathOperation(string $operationName, array $operation, string $method, string $operationType, string $resourceClass, ResourceMetadata $resourceMetadata, array $mimeTypes, \ArrayObject $definitions): \ArrayObject
236
    {
237
        $pathOperation = new \ArrayObject($operation['swagger_context'] ?? []);
238
        $resourceShortName = $resourceMetadata->getShortName();
239
        $pathOperation['tags'] ?? $pathOperation['tags'] = [$resourceShortName];
240
        $pathOperation['operationId'] ?? $pathOperation['operationId'] = lcfirst($operationName).ucfirst($resourceShortName).ucfirst($operationType);
241
242
        switch ($method) {
243
            case 'GET':
244
                return $this->updateGetOperation($pathOperation, $mimeTypes, $operationType, $resourceMetadata, $resourceClass, $resourceShortName, $operationName, $definitions);
245
            case 'POST':
246
                return $this->updatePostOperation($pathOperation, $mimeTypes, $operationType, $resourceMetadata, $resourceClass, $resourceShortName, $operationName, $definitions);
247
            case 'PUT':
0 ignored issues
show
Coding Style introduced by
case statements should be defined using a colon.

As per the PSR-2 coding standard, case statements should not be wrapped in curly braces. There is no need for braces, since each case is terminated by the next break.

There is also the option to use a semicolon instead of a colon, this is discouraged because many programmers do not even know it works and the colon is universal between programming languages.

switch ($expr) {
    case "A": { //wrong
        doSomething();
        break;
    }
    case "B"; //wrong
        doSomething();
        break;
    case "C": //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
248
                return $this->updatePutOperation($pathOperation, $mimeTypes, $operationType, $resourceMetadata, $resourceClass, $resourceShortName, $operationName, $definitions);
249
            case 'DELETE':
0 ignored issues
show
Coding Style introduced by
case statements should be defined using a colon.

As per the PSR-2 coding standard, case statements should not be wrapped in curly braces. There is no need for braces, since each case is terminated by the next break.

There is also the option to use a semicolon instead of a colon, this is discouraged because many programmers do not even know it works and the colon is universal between programming languages.

switch ($expr) {
    case "A": { //wrong
        doSomething();
        break;
    }
    case "B"; //wrong
        doSomething();
        break;
    case "C": //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
250
                return $this->updateDeleteOperation($pathOperation, $resourceShortName);
251
        }
252
253
        return $pathOperation;
254
    }
255
256
    /**
257
     * @param \ArrayObject     $pathOperation
258
     * @param array            $mimeTypes
259
     * @param string           $operationType
260
     * @param ResourceMetadata $resourceMetadata
261
     * @param string           $resourceClass
262
     * @param string           $resourceShortName
263
     * @param string           $operationName
264
     * @param \ArrayObject     $definitions
265
     *
266
     * @return \ArrayObject
267
     */
268
    private function updateGetOperation(\ArrayObject $pathOperation, array $mimeTypes, string $operationType, ResourceMetadata $resourceMetadata, string $resourceClass, string $resourceShortName, string $operationName, \ArrayObject $definitions)
269
    {
270
        $serializerContext = $this->getSerializerContext($operationType, false, $resourceMetadata, $operationName);
271
        $responseDefinitionKey = $this->getDefinition($definitions, $resourceMetadata, $resourceClass, $serializerContext);
272
273
        $pathOperation['produces'] ?? $pathOperation['produces'] = $mimeTypes;
274
275
        if ($operationType === OperationType::COLLECTION) {
276
            $pathOperation['summary'] ?? $pathOperation['summary'] = sprintf('Retrieves the collection of %s resources.', $resourceShortName);
277
            $pathOperation['responses'] ?? $pathOperation['responses'] = [
278
                '200' => [
279
                    'description' => sprintf('%s collection response', $resourceShortName),
280
                    'schema' => [
281
                        'type' => 'array',
282
                        'items' => ['$ref' => sprintf('#/definitions/%s', $responseDefinitionKey)],
283
                    ],
284
                ],
285
            ];
286
287 View Code Duplication
            if (!isset($pathOperation['parameters']) && $parameters = $this->getFiltersParameters($resourceClass, $operationName, $resourceMetadata, $definitions, $serializerContext)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
288
                $pathOperation['parameters'] = $parameters;
289
            }
290
291
            if ($this->paginationEnabled && $resourceMetadata->getCollectionOperationAttribute($operationName, 'pagination_enabled', true, true)) {
292
                $pathOperation['parameters'][] = $this->getPaginationParameters();
293
294
                if ($resourceMetadata->getCollectionOperationAttribute($operationName, 'pagination_client_items_per_page', $this->clientItemsPerPage, true)) {
295
                    $pathOperation['parameters'][] = $this->getItemsParPageParameters();
296
                }
297
            }
298
299
            return $pathOperation;
300
        }
301
302
        $pathOperation['summary'] ?? $pathOperation['summary'] = sprintf('Retrieves a %s resource.', $resourceShortName);
303
        $pathOperation['parameters'] ?? $pathOperation['parameters'] = [[
304
            'name' => 'id',
305
            'in' => 'path',
306
            'required' => true,
307
            'type' => 'string',
308
        ]];
309
        $pathOperation['responses'] ?? $pathOperation['responses'] = [
310
            '200' => [
311
                'description' => sprintf('%s resource response', $resourceShortName),
312
                'schema' => ['$ref' => sprintf('#/definitions/%s', $responseDefinitionKey)],
313
            ],
314
            '404' => ['description' => 'Resource not found'],
315
        ];
316
317
        return $pathOperation;
318
    }
319
320
    /**
321
     * @param \ArrayObject     $pathOperation
322
     * @param array            $mimeTypes
323
     * @param string           $operationType
324
     * @param ResourceMetadata $resourceMetadata
325
     * @param string           $resourceClass
326
     * @param string           $resourceShortName
327
     * @param string           $operationName
328
     * @param \ArrayObject     $definitions
329
     *
330
     * @return \ArrayObject
331
     */
332
    private function updatePostOperation(\ArrayObject $pathOperation, array $mimeTypes, string $operationType, ResourceMetadata $resourceMetadata, string $resourceClass, string $resourceShortName, string $operationName, \ArrayObject $definitions)
333
    {
334
        $pathOperation['consumes'] ?? $pathOperation['consumes'] = $mimeTypes;
335
        $pathOperation['produces'] ?? $pathOperation['produces'] = $mimeTypes;
336
        $pathOperation['summary'] ?? $pathOperation['summary'] = sprintf('Creates a %s resource.', $resourceShortName);
337
        $pathOperation['parameters'] ?? $pathOperation['parameters'] = [[
338
            'name' => lcfirst($resourceShortName),
339
            'in' => 'body',
340
            'description' => sprintf('The new %s resource', $resourceShortName),
341
            'schema' => ['$ref' => sprintf('#/definitions/%s', $this->getDefinition($definitions, $resourceMetadata, $resourceClass,
342
                $this->getSerializerContext($operationType, true, $resourceMetadata, $operationName)
343
            ))],
344
        ]];
345
        $pathOperation['responses'] ?? $pathOperation['responses'] = [
346
            '201' => [
347
                'description' => sprintf('%s resource created', $resourceShortName),
348
                'schema' => ['$ref' => sprintf('#/definitions/%s', $this->getDefinition($definitions, $resourceMetadata, $resourceClass,
349
                    $this->getSerializerContext($operationType, false, $resourceMetadata, $operationName)
350
                ))],
351
            ],
352
            '400' => ['description' => 'Invalid input'],
353
            '404' => ['description' => 'Resource not found'],
354
        ];
355
356
        return $pathOperation;
357
    }
358
359
    /**
360
     * @param \ArrayObject     $pathOperation
361
     * @param array            $mimeTypes
362
     * @param string           $operationType
363
     * @param ResourceMetadata $resourceMetadata
364
     * @param string           $resourceClass
365
     * @param string           $resourceShortName
366
     * @param string           $operationName
367
     * @param \ArrayObject     $definitions
368
     *
369
     * @return \ArrayObject
370
     */
371
    private function updatePutOperation(\ArrayObject $pathOperation, array $mimeTypes, string $operationType, ResourceMetadata $resourceMetadata, string $resourceClass, string $resourceShortName, string $operationName, \ArrayObject $definitions)
372
    {
373
        $pathOperation['consumes'] ?? $pathOperation['consumes'] = $mimeTypes;
374
        $pathOperation['produces'] ?? $pathOperation['produces'] = $mimeTypes;
375
        $pathOperation['summary'] ?? $pathOperation['summary'] = sprintf('Replaces the %s resource.', $resourceShortName);
376
        $pathOperation['parameters'] ?? $pathOperation['parameters'] = [
377
            [
378
                'name' => 'id',
379
                'in' => 'path',
380
                'type' => 'string',
381
                'required' => true,
382
            ],
383
            [
384
                'name' => lcfirst($resourceShortName),
385
                'in' => 'body',
386
                'description' => sprintf('The updated %s resource', $resourceShortName),
387
                'schema' => ['$ref' => sprintf('#/definitions/%s', $this->getDefinition($definitions, $resourceMetadata, $resourceClass,
388
                    $this->getSerializerContext($operationType, true, $resourceMetadata, $operationName)
389
                ))],
390
            ],
391
        ];
392
        $pathOperation['responses'] ?? $pathOperation['responses'] = [
393
            '200' => [
394
                'description' => sprintf('%s resource updated', $resourceShortName),
395
                'schema' => ['$ref' => sprintf('#/definitions/%s', $this->getDefinition($definitions, $resourceMetadata, $resourceClass,
396
                    $this->getSerializerContext($operationType, false, $resourceMetadata, $operationName)
397
                ))],
398
            ],
399
            '400' => ['description' => 'Invalid input'],
400
            '404' => ['description' => 'Resource not found'],
401
        ];
402
403
        return $pathOperation;
404
    }
405
406
    /**
407
     * @param \ArrayObject $pathOperation
408
     * @param string       $resourceShortName
409
     *
410
     * @return \ArrayObject
411
     */
412
    private function updateDeleteOperation(\ArrayObject $pathOperation, string $resourceShortName): \ArrayObject
413
    {
414
        $pathOperation['summary'] ?? $pathOperation['summary'] = sprintf('Removes the %s resource.', $resourceShortName);
415
        $pathOperation['responses'] ?? $pathOperation['responses'] = [
416
            '204' => ['description' => sprintf('%s resource deleted', $resourceShortName)],
417
            '404' => ['description' => 'Resource not found'],
418
        ];
419
420
        $pathOperation['parameters'] ?? $pathOperation['parameters'] = [[
421
            'name' => 'id',
422
            'in' => 'path',
423
            'type' => 'string',
424
            'required' => true,
425
        ]];
426
427
        return $pathOperation;
428
    }
429
430
    /**
431
     * @param \ArrayObject     $definitions
432
     * @param ResourceMetadata $resourceMetadata
433
     * @param string           $resourceClass
434
     * @param array|null       $serializerContext
435
     *
436
     * @return string
437
     */
438
    private function getDefinition(\ArrayObject $definitions, ResourceMetadata $resourceMetadata, string $resourceClass, array $serializerContext = null): string
439
    {
440
        $definitionKey = $this->getDefinitionKey($resourceMetadata->getShortName(), (array) ($serializerContext['groups'] ?? []));
441
442 View Code Duplication
        if (!isset($definitions[$definitionKey])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
443
            $definitions[$definitionKey] = [];  // Initialize first to prevent infinite loop
444
            $definitions[$definitionKey] = $this->getDefinitionSchema($resourceClass, $resourceMetadata, $definitions, $serializerContext);
445
        }
446
447
        return $definitionKey;
448
    }
449
450
    private function getDefinitionKey(string $resourceShortName, array $groups): string
451
    {
452
        return $groups ? sprintf('%s-%s', $resourceShortName, implode('_', $groups)) : $resourceShortName;
453
    }
454
455
    /**
456
     * Gets a definition Schema Object.
457
     *
458
     * @see https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#schemaObject
459
     *
460
     * @param string           $resourceClass
461
     * @param ResourceMetadata $resourceMetadata
462
     * @param \ArrayObject     $definitions
463
     * @param array|null       $serializerContext
464
     *
465
     * @return \ArrayObject
466
     */
467
    private function getDefinitionSchema(string $resourceClass, ResourceMetadata $resourceMetadata, \ArrayObject $definitions, array $serializerContext = null): \ArrayObject
468
    {
469
        $definitionSchema = new \ArrayObject(['type' => 'object']);
470
471
        if (null !== $description = $resourceMetadata->getDescription()) {
472
            $definitionSchema['description'] = $description;
473
        }
474
475
        if (null !== $iri = $resourceMetadata->getIri()) {
476
            $definitionSchema['externalDocs'] = ['url' => $iri];
477
        }
478
479
        $options = isset($serializerContext['groups']) ? ['serializer_groups' => $serializerContext['groups']] : [];
480
        foreach ($this->propertyNameCollectionFactory->create($resourceClass, $options) as $propertyName) {
481
            $propertyMetadata = $this->propertyMetadataFactory->create($resourceClass, $propertyName);
482
            $normalizedPropertyName = $this->nameConverter ? $this->nameConverter->normalize($propertyName) : $propertyName;
483
484
            if ($propertyMetadata->isRequired()) {
485
                $definitionSchema['required'][] = $normalizedPropertyName;
486
            }
487
488
            $definitionSchema['properties'][$normalizedPropertyName] = $this->getPropertySchema($propertyMetadata, $definitions, $serializerContext);
489
        }
490
491
        return $definitionSchema;
492
    }
493
494
    /**
495
     * Gets a property Schema Object.
496
     *
497
     * @see https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#schemaObject
498
     *
499
     * @param PropertyMetadata $propertyMetadata
500
     * @param \ArrayObject     $definitions
501
     * @param array|null       $serializerContext
502
     *
503
     * @return \ArrayObject
504
     */
505
    private function getPropertySchema(PropertyMetadata $propertyMetadata, \ArrayObject $definitions, array $serializerContext = null): \ArrayObject
506
    {
507
        $propertySchema = new \ArrayObject();
508
509
        if (false === $propertyMetadata->isWritable()) {
510
            $propertySchema['readOnly'] = true;
511
        }
512
513
        if (null !== $description = $propertyMetadata->getDescription()) {
514
            $propertySchema['description'] = $description;
515
        }
516
517
        if (null === $type = $propertyMetadata->getType()) {
518
            return $propertySchema;
519
        }
520
521
        $isCollection = $type->isCollection();
522
        if (null === $valueType = $isCollection ? $type->getCollectionValueType() : $type) {
523
            $builtinType = 'string';
524
            $className = null;
525
        } else {
526
            $builtinType = $valueType->getBuiltinType();
527
            $className = $valueType->getClassName();
528
        }
529
530
        $valueSchema = $this->getType($builtinType, $isCollection, $className, $propertyMetadata->isReadableLink(), $definitions, $serializerContext);
531
532
        return new \ArrayObject((array) $propertySchema + $valueSchema);
533
    }
534
535
    /**
536
     * Gets the Swagger's type corresponding to the given PHP's type.
537
     *
538
     * @param string       $type
539
     * @param bool         $isCollection
540
     * @param string       $className
541
     * @param bool         $readableLink
542
     * @param \ArrayObject $definitions
543
     * @param array|null   $serializerContext
544
     *
545
     * @return array
546
     */
547
    private function getType(string $type, bool $isCollection, string $className = null, bool $readableLink = null, \ArrayObject $definitions, array $serializerContext = null): array
548
    {
549
        if ($isCollection) {
550
            return ['type' => 'array', 'items' => $this->getType($type, false, $className, $readableLink, $definitions, $serializerContext)];
551
        }
552
553
        if (Type::BUILTIN_TYPE_STRING === $type) {
554
            return ['type' => 'string'];
555
        }
556
557
        if (Type::BUILTIN_TYPE_INT === $type) {
558
            return ['type' => 'integer'];
559
        }
560
561
        if (Type::BUILTIN_TYPE_FLOAT === $type) {
562
            return ['type' => 'number'];
563
        }
564
565
        if (Type::BUILTIN_TYPE_BOOL === $type) {
566
            return ['type' => 'boolean'];
567
        }
568
569
        if (Type::BUILTIN_TYPE_OBJECT === $type) {
570
            if (null === $className) {
571
                return ['type' => 'string'];
572
            }
573
574
            if (is_subclass_of($className, \DateTimeInterface::class)) {
575
                return ['type' => 'string', 'format' => 'date-time'];
576
            }
577
578
            if (!$this->resourceClassResolver->isResourceClass($className)) {
579
                return ['type' => 'string'];
580
            }
581
582
            if (true === $readableLink) {
583
                return ['$ref' => sprintf('#/definitions/%s', $this->getDefinition($definitions,
584
                    $this->resourceMetadataFactory->create($className),
585
                    $className, $serializerContext)
586
                )];
587
            }
588
        }
589
590
        return ['type' => 'string'];
591
    }
592
593
    /**
594
     * Computes the Swagger documentation.
595
     *
596
     * @param Documentation $documentation
597
     * @param \ArrayObject  $definitions
598
     * @param \ArrayObject  $paths
599
     * @param array         $context
600
     *
601
     * @return array
602
     */
603
    private function computeDoc(Documentation $documentation, \ArrayObject $definitions, \ArrayObject $paths, array $context): array
604
    {
605
        $doc = [
606
            'swagger' => self::SWAGGER_VERSION,
607
            'basePath' => $context['base_url'] ?? '/',
608
            'info' => [
609
                'title' => $documentation->getTitle(),
610
                'version' => $documentation->getVersion(),
611
            ],
612
            'paths' => $paths,
613
        ];
614
615
        if ($this->oauthEnabled) {
616
            $doc['securityDefinitions'] = [
617
                'oauth' => [
618
                    'type' => $this->oauthType,
619
                    'description' => 'OAuth client_credentials Grant',
620
                    'flow' => $this->oauthFlow,
621
                    'tokenUrl' => $this->oauthTokenUrl,
622
                    'authorizationUrl' => $this->oauthAuthorizationUrl,
623
                    'scopes' => $this->oauthScopes,
624
                ],
625
            ];
626
627
            $doc['security'] = [['oauth' => []]];
628
        }
629
630
        if ('' !== $description = $documentation->getDescription()) {
631
            $doc['info']['description'] = $description;
632
        }
633
634
        if (count($definitions) > 0) {
635
            $doc['definitions'] = $definitions;
636
        }
637
638
        return $doc;
639
    }
640
641
    /**
642
     * Gets Swagger parameters corresponding to enabled filters.
643
     *
644
     * @param string           $resourceClass
645
     * @param string           $operationName
646
     * @param ResourceMetadata $resourceMetadata
647
     * @param \ArrayObject     $definitions
648
     * @param array|null       $serializerContext
649
     *
650
     * @return array
651
     */
652
    private function getFiltersParameters(string $resourceClass, string $operationName, ResourceMetadata $resourceMetadata, \ArrayObject $definitions, array $serializerContext = null): array
653
    {
654
        if (null === $this->filterLocator) {
655
            return [];
656
        }
657
658
        $parameters = [];
659
        $resourceFilters = $resourceMetadata->getCollectionOperationAttribute($operationName, 'filters', [], true);
660
        foreach ($resourceFilters as $filterId) {
661
            if (!$filter = $this->getFilter($filterId)) {
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $filter is correct as $this->getFilter($filterId) (which targets ApiPlatform\Core\Api\Fil...catorTrait::getFilter()) seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
662
                continue;
663
            }
664
665
            foreach ($filter->getDescription($resourceClass) as $name => $data) {
666
                $parameter = [
667
                    'name' => $name,
668
                    'in' => 'query',
669
                    'required' => $data['required'],
670
                ];
671
                $parameter += $this->getType($data['type'], false, null, null, $definitions, $serializerContext);
672
673
                if (isset($data['swagger'])) {
674
                    $parameter = $data['swagger'] + $parameter;
675
                }
676
677
                $parameters[] = $parameter;
678
            }
679
        }
680
681
        return $parameters;
682
    }
683
684
    /**
685
     * Returns pagination parameters for the "get" collection operation.
686
     *
687
     * @return array
688
     */
689
    private function getPaginationParameters(): array
690
    {
691
        return [
692
            'name' => $this->paginationPageParameterName,
693
            'in' => 'query',
694
            'required' => false,
695
            'type' => 'integer',
696
            'description' => 'The collection page number',
697
        ];
698
    }
699
700
    /**
701
     * Returns items per page parameters for the "get" collection operation.
702
     *
703
     * @return array
704
     */
705
    private function getItemsParPageParameters(): array
706
    {
707
        return [
708
            'name' => $this->itemsPerPageParameterName,
709
            'in' => 'query',
710
            'required' => false,
711
            'type' => 'integer',
712
            'description' => 'The number of items per page',
713
        ];
714
    }
715
716
    /**
717
     * {@inheritdoc}
718
     */
719
    public function supportsNormalization($data, $format = null)
720
    {
721
        return self::FORMAT === $format && $data instanceof Documentation;
722
    }
723
724
    /**
725
     * @param string           $operationType
726
     * @param bool             $denormalization
727
     * @param ResourceMetadata $resourceMetadata
728
     * @param string           $operationType
729
     *
730
     * @return array|null
731
     */
732
    private function getSerializerContext(string $operationType, bool $denormalization, ResourceMetadata $resourceMetadata, string $operationName)
733
    {
734
        $contextKey = $denormalization ? 'denormalization_context' : 'normalization_context';
735
736
        if (OperationType::COLLECTION === $operationType) {
737
            return $resourceMetadata->getCollectionOperationAttribute($operationName, $contextKey, null, true);
738
        }
739
740
        return $resourceMetadata->getItemOperationAttribute($operationName, $contextKey, null, true);
741
    }
742
}
743