Completed
Push — master ( 4d39fe...29b346 )
by Amrouche
14s
created

DocumentationNormalizer::computeDoc()   C

Complexity

Conditions 9
Paths 32

Size

Total Lines 59
Code Lines 37

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 59
rs 6.9133
c 0
b 0
f 0
cc 9
eloc 37
nc 32
nop 4

How to fix   Long Method   

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\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 $apiKeys;
63
    private $subresourceOperationFactory;
64
    private $paginationEnabled;
65
    private $paginationPageParameterName;
66
    private $clientItemsPerPage;
67
    private $itemsPerPageParameterName;
68
69
    /**
70
     * @param ContainerInterface|FilterCollection|null $filterLocator The new filter locator or the deprecated filter collection
71
     */
72
    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 = '', array $oauthScopes = [], array $apiKeys = [], SubresourceOperationFactoryInterface $subresourceOperationFactory = null, $paginationEnabled = true, $paginationPageParameterName = 'page', $clientItemsPerPage = false, $itemsPerPageParameterName = 'itemsPerPage')
73
    {
74
        if ($urlGenerator) {
75
            @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...
76
        }
77
78
        $this->setFilterLocator($filterLocator, true);
79
80
        $this->resourceMetadataFactory = $resourceMetadataFactory;
81
        $this->propertyNameCollectionFactory = $propertyNameCollectionFactory;
82
        $this->propertyMetadataFactory = $propertyMetadataFactory;
83
        $this->resourceClassResolver = $resourceClassResolver;
84
        $this->operationMethodResolver = $operationMethodResolver;
85
        $this->operationPathResolver = $operationPathResolver;
86
        $this->nameConverter = $nameConverter;
87
        $this->oauthEnabled = $oauthEnabled;
88
        $this->oauthType = $oauthType;
89
        $this->oauthFlow = $oauthFlow;
90
        $this->oauthTokenUrl = $oauthTokenUrl;
91
        $this->oauthAuthorizationUrl = $oauthAuthorizationUrl;
92
        $this->oauthScopes = $oauthScopes;
93
        $this->subresourceOperationFactory = $subresourceOperationFactory;
94
        $this->paginationEnabled = $paginationEnabled;
95
        $this->paginationPageParameterName = $paginationPageParameterName;
96
        $this->apiKeys = $apiKeys;
97
        $this->subresourceOperationFactory = $subresourceOperationFactory;
98
        $this->clientItemsPerPage = $clientItemsPerPage;
99
        $this->itemsPerPageParameterName = $itemsPerPageParameterName;
100
    }
101
102
    /**
103
     * {@inheritdoc}
104
     */
105
    public function normalize($object, $format = null, array $context = [])
106
    {
107
        $mimeTypes = $object->getMimeTypes();
108
        $definitions = new \ArrayObject();
109
        $paths = new \ArrayObject();
110
111
        foreach ($object->getResourceNameCollection() as $resourceClass) {
112
            $resourceMetadata = $this->resourceMetadataFactory->create($resourceClass);
113
            $resourceShortName = $resourceMetadata->getShortName();
114
115
            $this->addPaths($paths, $definitions, $resourceClass, $resourceShortName, $resourceMetadata, $mimeTypes, OperationType::COLLECTION);
116
            $this->addPaths($paths, $definitions, $resourceClass, $resourceShortName, $resourceMetadata, $mimeTypes, OperationType::ITEM);
117
118
            if (null === $this->subresourceOperationFactory) {
119
                continue;
120
            }
121
122
            foreach ($this->subresourceOperationFactory->create($resourceClass) as $operationId => $subresourceOperation) {
123
                $operationName = 'get';
124
                $serializerContext = $this->getSerializerContext(OperationType::SUBRESOURCE, false, $resourceMetadata, $operationName);
125
                $responseDefinitionKey = $this->getDefinition($definitions, $this->resourceMetadataFactory->create($subresourceOperation['resource_class']), $subresourceOperation['resource_class'], $serializerContext);
126
127
                $pathOperation = new \ArrayObject([]);
128
                $pathOperation['tags'] = $subresourceOperation['shortNames'];
129
                $pathOperation['operationId'] = $operationId;
130
                $pathOperation['produces'] = $mimeTypes;
131
                $pathOperation['summary'] = sprintf('Retrieves %s%s resource%s.', $subresourceOperation['collection'] ? 'the collection of ' : 'a ', $subresourceOperation['shortNames'][0], $subresourceOperation['collection'] ? 's' : '');
132
                $pathOperation['responses'] = [
133
                    '200' => $subresourceOperation['collection'] ? [
134
                        'description' => sprintf('%s collection response', $subresourceOperation['shortNames'][0]),
135
                        'schema' => ['type' => 'array', 'items' => ['$ref' => sprintf('#/definitions/%s', $responseDefinitionKey)]],
136
                    ] : [
137
                        'description' => sprintf('%s resource response', $subresourceOperation['shortNames'][0]),
138
                        'schema' => ['$ref' => sprintf('#/definitions/%s', $responseDefinitionKey)],
139
                    ],
140
                    '404' => ['description' => 'Resource not found'],
141
                ];
142
143
                // Avoid duplicates parameters when there is a filter on a subresource identifier
144
                $parametersMemory = [];
145
                $pathOperation['parameters'] = [];
146
147
                foreach ($subresourceOperation['identifiers'] as list($identifier, , $hasIdentifier)) {
148
                    if (true === $hasIdentifier) {
149
                        $pathOperation['parameters'][] = ['name' => $identifier, 'in' => 'path', 'required' => true, 'type' => 'string'];
150
                        $parametersMemory[] = $identifier;
151
                    }
152
                }
153
154
                if ($parameters = $this->getFiltersParameters($resourceClass, $operationName, $resourceMetadata, $definitions, $serializerContext)) {
155
                    foreach ($parameters as $parameter) {
156
                        if (!in_array($parameter['name'], $parametersMemory, true)) {
157
                            $pathOperation['parameters'][] = $parameter;
158
                        }
159
                    }
160
                }
161
162
                $paths[$this->getPath($subresourceOperation['shortNames'][0], $subresourceOperation['route_name'], $subresourceOperation, OperationType::SUBRESOURCE)] = new \ArrayObject(['get' => $pathOperation]);
163
            }
164
        }
165
166
        $definitions->ksort();
167
        $paths->ksort();
168
169
        return $this->computeDoc($object, $definitions, $paths, $context);
170
    }
171
172
    /**
173
     * Updates the list of entries in the paths collection.
174
     *
175
     * @param \ArrayObject     $paths
176
     * @param \ArrayObject     $definitions
177
     * @param string           $resourceClass
178
     * @param string           $resourceShortName
179
     * @param ResourceMetadata $resourceMetadata
180
     * @param array            $mimeTypes
181
     * @param string           $operationType
182
     */
183
    private function addPaths(\ArrayObject $paths, \ArrayObject $definitions, string $resourceClass, string $resourceShortName, ResourceMetadata $resourceMetadata, array $mimeTypes, string $operationType)
184
    {
185
        if (null === $operations = $operationType === OperationType::COLLECTION ? $resourceMetadata->getCollectionOperations() : $resourceMetadata->getItemOperations()) {
186
            return;
187
        }
188
189
        foreach ($operations as $operationName => $operation) {
190
            $path = $this->getPath($resourceShortName, $operationName, $operation, $operationType);
191
            $method = $operationType === OperationType::ITEM ? $this->operationMethodResolver->getItemOperationMethod($resourceClass, $operationName) : $this->operationMethodResolver->getCollectionOperationMethod($resourceClass, $operationName);
192
193
            $paths[$path][strtolower($method)] = $this->getPathOperation($operationName, $operation, $method, $operationType, $resourceClass, $resourceMetadata, $mimeTypes, $definitions);
194
        }
195
    }
196
197
    /**
198
     * Gets the path for an operation.
199
     *
200
     * If the path ends with the optional _format parameter, it is removed
201
     * as optional path parameters are not yet supported.
202
     *
203
     * @see https://github.com/OAI/OpenAPI-Specification/issues/93
204
     *
205
     * @param string $resourceShortName
206
     * @param string $operationName
207
     * @param array  $operation
208
     * @param string $operationType
209
     *
210
     * @return string
211
     */
212
    private function getPath(string $resourceShortName, string $operationName, array $operation, string $operationType): string
213
    {
214
        $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...
215
        if ('.{_format}' === substr($path, -10)) {
216
            $path = substr($path, 0, -10);
217
        }
218
219
        return $path;
220
    }
221
222
    /**
223
     * Gets a path Operation Object.
224
     *
225
     * @see https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#operation-object
226
     *
227
     * @param string           $operationName
228
     * @param array            $operation
229
     * @param string           $method
230
     * @param string           $operationType
231
     * @param string           $resourceClass
232
     * @param ResourceMetadata $resourceMetadata
233
     * @param string[]         $mimeTypes
234
     * @param \ArrayObject     $definitions
235
     *
236
     * @return \ArrayObject
237
     */
238
    private function getPathOperation(string $operationName, array $operation, string $method, string $operationType, string $resourceClass, ResourceMetadata $resourceMetadata, array $mimeTypes, \ArrayObject $definitions): \ArrayObject
239
    {
240
        $pathOperation = new \ArrayObject($operation['swagger_context'] ?? []);
241
        $resourceShortName = $resourceMetadata->getShortName();
242
        $pathOperation['tags'] ?? $pathOperation['tags'] = [$resourceShortName];
243
        $pathOperation['operationId'] ?? $pathOperation['operationId'] = lcfirst($operationName).ucfirst($resourceShortName).ucfirst($operationType);
244
245
        switch ($method) {
246
            case 'GET':
247
                return $this->updateGetOperation($pathOperation, $mimeTypes, $operationType, $resourceMetadata, $resourceClass, $resourceShortName, $operationName, $definitions);
248
            case 'POST':
249
                return $this->updatePostOperation($pathOperation, $mimeTypes, $operationType, $resourceMetadata, $resourceClass, $resourceShortName, $operationName, $definitions);
250
            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...
251
                return $this->updatePutOperation($pathOperation, $mimeTypes, $operationType, $resourceMetadata, $resourceClass, $resourceShortName, $operationName, $definitions);
252
            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...
253
                return $this->updateDeleteOperation($pathOperation, $resourceShortName);
254
        }
255
256
        return $pathOperation;
257
    }
258
259
    /**
260
     * @param \ArrayObject     $pathOperation
261
     * @param array            $mimeTypes
262
     * @param string           $operationType
263
     * @param ResourceMetadata $resourceMetadata
264
     * @param string           $resourceClass
265
     * @param string           $resourceShortName
266
     * @param string           $operationName
267
     * @param \ArrayObject     $definitions
268
     *
269
     * @return \ArrayObject
270
     */
271
    private function updateGetOperation(\ArrayObject $pathOperation, array $mimeTypes, string $operationType, ResourceMetadata $resourceMetadata, string $resourceClass, string $resourceShortName, string $operationName, \ArrayObject $definitions)
272
    {
273
        $serializerContext = $this->getSerializerContext($operationType, false, $resourceMetadata, $operationName);
274
        $responseDefinitionKey = $this->getDefinition($definitions, $resourceMetadata, $resourceClass, $serializerContext);
275
276
        $pathOperation['produces'] ?? $pathOperation['produces'] = $mimeTypes;
277
278
        if ($operationType === OperationType::COLLECTION) {
279
            $pathOperation['summary'] ?? $pathOperation['summary'] = sprintf('Retrieves the collection of %s resources.', $resourceShortName);
280
            $pathOperation['responses'] ?? $pathOperation['responses'] = [
281
                '200' => [
282
                    'description' => sprintf('%s collection response', $resourceShortName),
283
                    'schema' => [
284
                        'type' => 'array',
285
                        'items' => ['$ref' => sprintf('#/definitions/%s', $responseDefinitionKey)],
286
                    ],
287
                ],
288
            ];
289
290 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...
291
                $pathOperation['parameters'] = $parameters;
292
            }
293
294
            if ($this->paginationEnabled && $resourceMetadata->getCollectionOperationAttribute($operationName, 'pagination_enabled', true, true)) {
295
                $pathOperation['parameters'][] = $this->getPaginationParameters();
296
297
                if ($resourceMetadata->getCollectionOperationAttribute($operationName, 'pagination_client_items_per_page', $this->clientItemsPerPage, true)) {
298
                    $pathOperation['parameters'][] = $this->getItemsParPageParameters();
299
                }
300
            }
301
302
            return $pathOperation;
303
        }
304
305
        $pathOperation['summary'] ?? $pathOperation['summary'] = sprintf('Retrieves a %s resource.', $resourceShortName);
306
        $pathOperation['parameters'] ?? $pathOperation['parameters'] = [[
307
            'name' => 'id',
308
            'in' => 'path',
309
            'required' => true,
310
            'type' => 'string',
311
        ]];
312
        $pathOperation['responses'] ?? $pathOperation['responses'] = [
313
            '200' => [
314
                'description' => sprintf('%s resource response', $resourceShortName),
315
                'schema' => ['$ref' => sprintf('#/definitions/%s', $responseDefinitionKey)],
316
            ],
317
            '404' => ['description' => 'Resource not found'],
318
        ];
319
320
        return $pathOperation;
321
    }
322
323
    /**
324
     * @param \ArrayObject     $pathOperation
325
     * @param array            $mimeTypes
326
     * @param string           $operationType
327
     * @param ResourceMetadata $resourceMetadata
328
     * @param string           $resourceClass
329
     * @param string           $resourceShortName
330
     * @param string           $operationName
331
     * @param \ArrayObject     $definitions
332
     *
333
     * @return \ArrayObject
334
     */
335
    private function updatePostOperation(\ArrayObject $pathOperation, array $mimeTypes, string $operationType, ResourceMetadata $resourceMetadata, string $resourceClass, string $resourceShortName, string $operationName, \ArrayObject $definitions)
336
    {
337
        $pathOperation['consumes'] ?? $pathOperation['consumes'] = $mimeTypes;
338
        $pathOperation['produces'] ?? $pathOperation['produces'] = $mimeTypes;
339
        $pathOperation['summary'] ?? $pathOperation['summary'] = sprintf('Creates a %s resource.', $resourceShortName);
340
        $pathOperation['parameters'] ?? $pathOperation['parameters'] = [[
341
            'name' => lcfirst($resourceShortName),
342
            'in' => 'body',
343
            'description' => sprintf('The new %s resource', $resourceShortName),
344
            'schema' => ['$ref' => sprintf('#/definitions/%s', $this->getDefinition($definitions, $resourceMetadata, $resourceClass,
345
                $this->getSerializerContext($operationType, true, $resourceMetadata, $operationName)
346
            ))],
347
        ]];
348
        $pathOperation['responses'] ?? $pathOperation['responses'] = [
349
            '201' => [
350
                'description' => sprintf('%s resource created', $resourceShortName),
351
                'schema' => ['$ref' => sprintf('#/definitions/%s', $this->getDefinition($definitions, $resourceMetadata, $resourceClass,
352
                    $this->getSerializerContext($operationType, false, $resourceMetadata, $operationName)
353
                ))],
354
            ],
355
            '400' => ['description' => 'Invalid input'],
356
            '404' => ['description' => 'Resource not found'],
357
        ];
358
359
        return $pathOperation;
360
    }
361
362
    /**
363
     * @param \ArrayObject     $pathOperation
364
     * @param array            $mimeTypes
365
     * @param string           $operationType
366
     * @param ResourceMetadata $resourceMetadata
367
     * @param string           $resourceClass
368
     * @param string           $resourceShortName
369
     * @param string           $operationName
370
     * @param \ArrayObject     $definitions
371
     *
372
     * @return \ArrayObject
373
     */
374
    private function updatePutOperation(\ArrayObject $pathOperation, array $mimeTypes, string $operationType, ResourceMetadata $resourceMetadata, string $resourceClass, string $resourceShortName, string $operationName, \ArrayObject $definitions)
375
    {
376
        $pathOperation['consumes'] ?? $pathOperation['consumes'] = $mimeTypes;
377
        $pathOperation['produces'] ?? $pathOperation['produces'] = $mimeTypes;
378
        $pathOperation['summary'] ?? $pathOperation['summary'] = sprintf('Replaces the %s resource.', $resourceShortName);
379
        $pathOperation['parameters'] ?? $pathOperation['parameters'] = [
380
            [
381
                'name' => 'id',
382
                'in' => 'path',
383
                'type' => 'string',
384
                'required' => true,
385
            ],
386
            [
387
                'name' => lcfirst($resourceShortName),
388
                'in' => 'body',
389
                'description' => sprintf('The updated %s resource', $resourceShortName),
390
                'schema' => ['$ref' => sprintf('#/definitions/%s', $this->getDefinition($definitions, $resourceMetadata, $resourceClass,
391
                    $this->getSerializerContext($operationType, true, $resourceMetadata, $operationName)
392
                ))],
393
            ],
394
        ];
395
        $pathOperation['responses'] ?? $pathOperation['responses'] = [
396
            '200' => [
397
                'description' => sprintf('%s resource updated', $resourceShortName),
398
                'schema' => ['$ref' => sprintf('#/definitions/%s', $this->getDefinition($definitions, $resourceMetadata, $resourceClass,
399
                    $this->getSerializerContext($operationType, false, $resourceMetadata, $operationName)
400
                ))],
401
            ],
402
            '400' => ['description' => 'Invalid input'],
403
            '404' => ['description' => 'Resource not found'],
404
        ];
405
406
        return $pathOperation;
407
    }
408
409
    /**
410
     * @param \ArrayObject $pathOperation
411
     * @param string       $resourceShortName
412
     *
413
     * @return \ArrayObject
414
     */
415
    private function updateDeleteOperation(\ArrayObject $pathOperation, string $resourceShortName): \ArrayObject
416
    {
417
        $pathOperation['summary'] ?? $pathOperation['summary'] = sprintf('Removes the %s resource.', $resourceShortName);
418
        $pathOperation['responses'] ?? $pathOperation['responses'] = [
419
            '204' => ['description' => sprintf('%s resource deleted', $resourceShortName)],
420
            '404' => ['description' => 'Resource not found'],
421
        ];
422
423
        $pathOperation['parameters'] ?? $pathOperation['parameters'] = [[
424
            'name' => 'id',
425
            'in' => 'path',
426
            'type' => 'string',
427
            'required' => true,
428
        ]];
429
430
        return $pathOperation;
431
    }
432
433
    /**
434
     * @param \ArrayObject     $definitions
435
     * @param ResourceMetadata $resourceMetadata
436
     * @param string           $resourceClass
437
     * @param array|null       $serializerContext
438
     *
439
     * @return string
440
     */
441
    private function getDefinition(\ArrayObject $definitions, ResourceMetadata $resourceMetadata, string $resourceClass, array $serializerContext = null): string
442
    {
443
        $definitionKey = $this->getDefinitionKey($resourceMetadata->getShortName(), (array) ($serializerContext['groups'] ?? []));
444
445 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...
446
            $definitions[$definitionKey] = [];  // Initialize first to prevent infinite loop
447
            $definitions[$definitionKey] = $this->getDefinitionSchema($resourceClass, $resourceMetadata, $definitions, $serializerContext);
448
        }
449
450
        return $definitionKey;
451
    }
452
453
    private function getDefinitionKey(string $resourceShortName, array $groups): string
454
    {
455
        return $groups ? sprintf('%s-%s', $resourceShortName, implode('_', $groups)) : $resourceShortName;
456
    }
457
458
    /**
459
     * Gets a definition Schema Object.
460
     *
461
     * @see https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#schemaObject
462
     *
463
     * @param string           $resourceClass
464
     * @param ResourceMetadata $resourceMetadata
465
     * @param \ArrayObject     $definitions
466
     * @param array|null       $serializerContext
467
     *
468
     * @return \ArrayObject
469
     */
470
    private function getDefinitionSchema(string $resourceClass, ResourceMetadata $resourceMetadata, \ArrayObject $definitions, array $serializerContext = null): \ArrayObject
471
    {
472
        $definitionSchema = new \ArrayObject(['type' => 'object']);
473
474
        if (null !== $description = $resourceMetadata->getDescription()) {
475
            $definitionSchema['description'] = $description;
476
        }
477
478
        if (null !== $iri = $resourceMetadata->getIri()) {
479
            $definitionSchema['externalDocs'] = ['url' => $iri];
480
        }
481
482
        $options = isset($serializerContext['groups']) ? ['serializer_groups' => $serializerContext['groups']] : [];
483
        foreach ($this->propertyNameCollectionFactory->create($resourceClass, $options) as $propertyName) {
484
            $propertyMetadata = $this->propertyMetadataFactory->create($resourceClass, $propertyName);
485
            $normalizedPropertyName = $this->nameConverter ? $this->nameConverter->normalize($propertyName) : $propertyName;
486
487
            if ($propertyMetadata->isRequired()) {
488
                $definitionSchema['required'][] = $normalizedPropertyName;
489
            }
490
491
            $definitionSchema['properties'][$normalizedPropertyName] = $this->getPropertySchema($propertyMetadata, $definitions, $serializerContext);
492
        }
493
494
        return $definitionSchema;
495
    }
496
497
    /**
498
     * Gets a property Schema Object.
499
     *
500
     * @see https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#schemaObject
501
     *
502
     * @param PropertyMetadata $propertyMetadata
503
     * @param \ArrayObject     $definitions
504
     * @param array|null       $serializerContext
505
     *
506
     * @return \ArrayObject
507
     */
508
    private function getPropertySchema(PropertyMetadata $propertyMetadata, \ArrayObject $definitions, array $serializerContext = null): \ArrayObject
509
    {
510
        $propertySchema = new \ArrayObject();
511
512
        if (false === $propertyMetadata->isWritable()) {
513
            $propertySchema['readOnly'] = true;
514
        }
515
516
        if (null !== $description = $propertyMetadata->getDescription()) {
517
            $propertySchema['description'] = $description;
518
        }
519
520
        if (null === $type = $propertyMetadata->getType()) {
521
            return $propertySchema;
522
        }
523
524
        $isCollection = $type->isCollection();
525
        if (null === $valueType = $isCollection ? $type->getCollectionValueType() : $type) {
526
            $builtinType = 'string';
527
            $className = null;
528
        } else {
529
            $builtinType = $valueType->getBuiltinType();
530
            $className = $valueType->getClassName();
531
        }
532
533
        $valueSchema = $this->getType($builtinType, $isCollection, $className, $propertyMetadata->isReadableLink(), $definitions, $serializerContext);
534
535
        return new \ArrayObject((array) $propertySchema + $valueSchema);
536
    }
537
538
    /**
539
     * Gets the Swagger's type corresponding to the given PHP's type.
540
     *
541
     * @param string       $type
542
     * @param bool         $isCollection
543
     * @param string       $className
544
     * @param bool         $readableLink
545
     * @param \ArrayObject $definitions
546
     * @param array|null   $serializerContext
547
     *
548
     * @return array
549
     */
550
    private function getType(string $type, bool $isCollection, string $className = null, bool $readableLink = null, \ArrayObject $definitions, array $serializerContext = null): array
551
    {
552
        if ($isCollection) {
553
            return ['type' => 'array', 'items' => $this->getType($type, false, $className, $readableLink, $definitions, $serializerContext)];
554
        }
555
556
        if (Type::BUILTIN_TYPE_STRING === $type) {
557
            return ['type' => 'string'];
558
        }
559
560
        if (Type::BUILTIN_TYPE_INT === $type) {
561
            return ['type' => 'integer'];
562
        }
563
564
        if (Type::BUILTIN_TYPE_FLOAT === $type) {
565
            return ['type' => 'number'];
566
        }
567
568
        if (Type::BUILTIN_TYPE_BOOL === $type) {
569
            return ['type' => 'boolean'];
570
        }
571
572
        if (Type::BUILTIN_TYPE_OBJECT === $type) {
573
            if (null === $className) {
574
                return ['type' => 'string'];
575
            }
576
577
            if (is_subclass_of($className, \DateTimeInterface::class)) {
578
                return ['type' => 'string', 'format' => 'date-time'];
579
            }
580
581
            if (!$this->resourceClassResolver->isResourceClass($className)) {
582
                return ['type' => 'string'];
583
            }
584
585
            if (true === $readableLink) {
586
                return ['$ref' => sprintf('#/definitions/%s', $this->getDefinition($definitions,
587
                    $this->resourceMetadataFactory->create($className),
588
                    $className, $serializerContext)
589
                )];
590
            }
591
        }
592
593
        return ['type' => 'string'];
594
    }
595
596
    /**
597
     * Computes the Swagger documentation.
598
     *
599
     * @param Documentation $documentation
600
     * @param \ArrayObject  $definitions
601
     * @param \ArrayObject  $paths
602
     * @param array         $context
603
     *
604
     * @return array
605
     */
606
    private function computeDoc(Documentation $documentation, \ArrayObject $definitions, \ArrayObject $paths, array $context): array
607
    {
608
        $doc = [
609
            'swagger' => self::SWAGGER_VERSION,
610
            'basePath' => $context['base_url'] ?? '/',
611
            'info' => [
612
                'title' => $documentation->getTitle(),
613
                'version' => $documentation->getVersion(),
614
            ],
615
            'paths' => $paths,
616
        ];
617
618
        $securityDefinitions = [];
619
        $security = [];
620
621
        if ($this->oauthEnabled) {
622
            $securityDefinitions['oauth'] = [
623
                'type' => $this->oauthType,
624
                'description' => 'OAuth client_credentials Grant',
625
                'flow' => $this->oauthFlow,
626
                'tokenUrl' => $this->oauthTokenUrl,
627
                'authorizationUrl' => $this->oauthAuthorizationUrl,
628
                'scopes' => $this->oauthScopes,
629
            ];
630
631
            $security[] = ['oauth' => []];
632
        }
633
634
        if ($this->apiKeys) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->apiKeys of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
635
            foreach ($this->apiKeys as $key => $apiKey) {
636
                $name = $apiKey['name'];
637
                $type = $apiKey['type'];
638
639
                $securityDefinitions[$key] = [
640
                    'type' => 'apiKey',
641
                    'in' => $type,
642
                    'description' => sprintf('Value for the %s %s', $name, $type === 'query' ? sprintf('%s parameter', $type) : $type),
643
                    'name' => $name,
644
                ];
645
646
                $security[] = [$key => []];
647
            }
648
        }
649
650
        if ($securityDefinitions && $security) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $securityDefinitions of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
Bug Best Practice introduced by
The expression $security of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
651
            $doc['securityDefinitions'] = $securityDefinitions;
652
            $doc['security'] = $security;
653
        }
654
655
        if ('' !== $description = $documentation->getDescription()) {
656
            $doc['info']['description'] = $description;
657
        }
658
659
        if (count($definitions) > 0) {
660
            $doc['definitions'] = $definitions;
661
        }
662
663
        return $doc;
664
    }
665
666
    /**
667
     * Gets Swagger parameters corresponding to enabled filters.
668
     *
669
     * @param string           $resourceClass
670
     * @param string           $operationName
671
     * @param ResourceMetadata $resourceMetadata
672
     * @param \ArrayObject     $definitions
673
     * @param array|null       $serializerContext
674
     *
675
     * @return array
676
     */
677
    private function getFiltersParameters(string $resourceClass, string $operationName, ResourceMetadata $resourceMetadata, \ArrayObject $definitions, array $serializerContext = null): array
678
    {
679
        if (null === $this->filterLocator) {
680
            return [];
681
        }
682
683
        $parameters = [];
684
        $resourceFilters = $resourceMetadata->getCollectionOperationAttribute($operationName, 'filters', [], true);
685
        foreach ($resourceFilters as $filterId) {
686
            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...
687
                continue;
688
            }
689
690
            foreach ($filter->getDescription($resourceClass) as $name => $data) {
691
                $parameter = [
692
                    'name' => $name,
693
                    'in' => 'query',
694
                    'required' => $data['required'],
695
                ];
696
                $parameter += $this->getType($data['type'], false, null, null, $definitions, $serializerContext);
697
698
                if (isset($data['swagger'])) {
699
                    $parameter = $data['swagger'] + $parameter;
700
                }
701
702
                $parameters[] = $parameter;
703
            }
704
        }
705
706
        return $parameters;
707
    }
708
709
    /**
710
     * Returns pagination parameters for the "get" collection operation.
711
     *
712
     * @return array
713
     */
714
    private function getPaginationParameters(): array
715
    {
716
        return [
717
            'name' => $this->paginationPageParameterName,
718
            'in' => 'query',
719
            'required' => false,
720
            'type' => 'integer',
721
            'description' => 'The collection page number',
722
        ];
723
    }
724
725
    /**
726
     * Returns items per page parameters for the "get" collection operation.
727
     *
728
     * @return array
729
     */
730
    private function getItemsParPageParameters(): array
731
    {
732
        return [
733
            'name' => $this->itemsPerPageParameterName,
734
            'in' => 'query',
735
            'required' => false,
736
            'type' => 'integer',
737
            'description' => 'The number of items per page',
738
        ];
739
    }
740
741
    /**
742
     * {@inheritdoc}
743
     */
744
    public function supportsNormalization($data, $format = null)
745
    {
746
        return self::FORMAT === $format && $data instanceof Documentation;
747
    }
748
749
    /**
750
     * @param string           $operationType
751
     * @param bool             $denormalization
752
     * @param ResourceMetadata $resourceMetadata
753
     * @param string           $operationType
754
     *
755
     * @return array|null
756
     */
757
    private function getSerializerContext(string $operationType, bool $denormalization, ResourceMetadata $resourceMetadata, string $operationName)
758
    {
759
        $contextKey = $denormalization ? 'denormalization_context' : 'normalization_context';
760
761
        if (OperationType::COLLECTION === $operationType) {
762
            return $resourceMetadata->getCollectionOperationAttribute($operationName, $contextKey, null, true);
763
        }
764
765
        return $resourceMetadata->getItemOperationAttribute($operationName, $contextKey, null, true);
766
    }
767
}
768