Passed
Pull Request — master (#2171)
by
unknown
05:17 queued 02:38
created

DocumentationNormalizer   F

Complexity

Total Complexity 93

Size/Duplication

Total Lines 657
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 93
eloc 337
dl 0
loc 657
rs 2
c 0
b 0
f 0

22 Methods

Rating   Name   Duplication   Size   Complexity  
B getType() 0 44 11
A getItemsPerPageParameters() 0 8 1
A getPath() 0 8 2
A getPaginationClientEnabledParameters() 0 8 1
A updateDeleteOperation() 0 16 1
A getPropertySchema() 0 28 6
A updatePutOperation() 0 33 1
A __construct() 0 30 2
C normalize() 0 66 12
A addPaths() 0 11 5
B computeDoc() 0 58 9
A getDefinitionKey() 0 3 2
A getPaginationParameters() 0 8 1
B getPathOperation() 0 25 7
A updatePostOperation() 0 25 1
A getDefinition() 0 14 3
A supportsNormalization() 0 3 2
B updateGetOperation() 0 53 9
A getSerializerContext() 0 9 3
A getFiltersParameters() 0 30 6
B getDefinitionSchema() 0 25 7
A hasCacheableSupportsMethod() 0 3 1

How to fix   Complexity   

Complex Class

Complex classes like DocumentationNormalizer often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use DocumentationNormalizer, and based on these observations, apply Extract Interface, too.

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\AbstractNormalizer;
34
use Symfony\Component\Serializer\Normalizer\CacheableSupportsMethodInterface;
35
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
36
37
/**
38
 * Creates a machine readable Swagger API documentation.
39
 *
40
 * @author Amrouche Hamza <[email protected]>
41
 * @author Teoh Han Hui <[email protected]>
42
 * @author Kévin Dunglas <[email protected]>
43
 */
44
final class DocumentationNormalizer implements NormalizerInterface, CacheableSupportsMethodInterface
45
{
46
    use FilterLocatorTrait;
47
48
    const SWAGGER_VERSION = '2.0';
49
    const FORMAT = 'json';
50
    const SWAGGER_DEFINITION_NAME = 'swagger_definition_name';
51
52
    private $resourceMetadataFactory;
53
    private $propertyNameCollectionFactory;
54
    private $propertyMetadataFactory;
55
    private $resourceClassResolver;
56
    private $operationMethodResolver;
57
    private $operationPathResolver;
58
    private $nameConverter;
59
    private $oauthEnabled;
60
    private $oauthType;
61
    private $oauthFlow;
62
    private $oauthTokenUrl;
63
    private $oauthAuthorizationUrl;
64
    private $oauthScopes;
65
    private $apiKeys;
66
    private $subresourceOperationFactory;
67
    private $paginationEnabled;
68
    private $paginationPageParameterName;
69
    private $clientItemsPerPage;
70
    private $itemsPerPageParameterName;
71
    private $paginationClientEnabled;
72
    private $paginationClientEnabledParameterName;
73
74
    /**
75
     * @param ContainerInterface|FilterCollection|null $filterLocator The new filter locator or the deprecated filter collection
76
     */
77
    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', $paginationClientEnabled = false, $paginationClientEnabledParameterName = 'pagination')
78
    {
79
        if ($urlGenerator) {
80
            @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);
81
        }
82
83
        $this->setFilterLocator($filterLocator, true);
84
85
        $this->resourceMetadataFactory = $resourceMetadataFactory;
86
        $this->propertyNameCollectionFactory = $propertyNameCollectionFactory;
87
        $this->propertyMetadataFactory = $propertyMetadataFactory;
88
        $this->resourceClassResolver = $resourceClassResolver;
89
        $this->operationMethodResolver = $operationMethodResolver;
90
        $this->operationPathResolver = $operationPathResolver;
91
        $this->nameConverter = $nameConverter;
92
        $this->oauthEnabled = $oauthEnabled;
93
        $this->oauthType = $oauthType;
94
        $this->oauthFlow = $oauthFlow;
95
        $this->oauthTokenUrl = $oauthTokenUrl;
96
        $this->oauthAuthorizationUrl = $oauthAuthorizationUrl;
97
        $this->oauthScopes = $oauthScopes;
98
        $this->subresourceOperationFactory = $subresourceOperationFactory;
99
        $this->paginationEnabled = $paginationEnabled;
100
        $this->paginationPageParameterName = $paginationPageParameterName;
101
        $this->apiKeys = $apiKeys;
102
        $this->subresourceOperationFactory = $subresourceOperationFactory;
103
        $this->clientItemsPerPage = $clientItemsPerPage;
104
        $this->itemsPerPageParameterName = $itemsPerPageParameterName;
105
        $this->paginationClientEnabled = $paginationClientEnabled;
106
        $this->paginationClientEnabledParameterName = $paginationClientEnabledParameterName;
107
    }
108
109
    /**
110
     * {@inheritdoc}
111
     */
112
    public function normalize($object, $format = null, array $context = [])
113
    {
114
        $mimeTypes = $object->getMimeTypes();
115
        $definitions = new \ArrayObject();
116
        $paths = new \ArrayObject();
117
118
        foreach ($object->getResourceNameCollection() as $resourceClass) {
119
            $resourceMetadata = $this->resourceMetadataFactory->create($resourceClass);
120
            $resourceShortName = $resourceMetadata->getShortName();
121
122
            $this->addPaths($paths, $definitions, $resourceClass, $resourceShortName, $resourceMetadata, $mimeTypes, OperationType::COLLECTION);
123
            $this->addPaths($paths, $definitions, $resourceClass, $resourceShortName, $resourceMetadata, $mimeTypes, OperationType::ITEM);
124
125
            if (null === $this->subresourceOperationFactory) {
126
                continue;
127
            }
128
129
            foreach ($this->subresourceOperationFactory->create($resourceClass) as $operationId => $subresourceOperation) {
130
                $operationName = 'get';
131
                $subResourceMetadata = $this->resourceMetadataFactory->create($subresourceOperation['resource_class']);
132
                $serializerContext = $this->getSerializerContext(OperationType::SUBRESOURCE, false, $subResourceMetadata, $operationName);
133
                $responseDefinitionKey = $this->getDefinition($definitions, $subResourceMetadata, $subresourceOperation['resource_class'], $serializerContext);
134
135
                $pathOperation = new \ArrayObject([]);
136
                $pathOperation['tags'] = $subresourceOperation['shortNames'];
137
                $pathOperation['operationId'] = $operationId;
138
                $pathOperation['produces'] = $mimeTypes;
139
                $pathOperation['summary'] = sprintf('Retrieves %s%s resource%s.', $subresourceOperation['collection'] ? 'the collection of ' : 'a ', $subresourceOperation['shortNames'][0], $subresourceOperation['collection'] ? 's' : '');
140
                $pathOperation['responses'] = [
141
                    '200' => $subresourceOperation['collection'] ? [
142
                        'description' => sprintf('%s collection response', $subresourceOperation['shortNames'][0]),
143
                        'schema' => ['type' => 'array', 'items' => ['$ref' => sprintf('#/definitions/%s', $responseDefinitionKey)]],
144
                    ] : [
145
                        'description' => sprintf('%s resource response', $subresourceOperation['shortNames'][0]),
146
                        'schema' => ['$ref' => sprintf('#/definitions/%s', $responseDefinitionKey)],
147
                    ],
148
                    '404' => ['description' => 'Resource not found'],
149
                ];
150
151
                // Avoid duplicates parameters when there is a filter on a subresource identifier
152
                $parametersMemory = [];
153
                $pathOperation['parameters'] = [];
154
155
                foreach ($subresourceOperation['identifiers'] as list($identifier, , $hasIdentifier)) {
156
                    if (true === $hasIdentifier) {
157
                        $pathOperation['parameters'][] = ['name' => $identifier, 'in' => 'path', 'required' => true, 'type' => 'string'];
158
                        $parametersMemory[] = $identifier;
159
                    }
160
                }
161
162
                if ($parameters = $this->getFiltersParameters($subresourceOperation['resource_class'], $operationName, $subResourceMetadata, $definitions, $serializerContext)) {
163
                    foreach ($parameters as $parameter) {
164
                        if (!\in_array($parameter['name'], $parametersMemory, true)) {
165
                            $pathOperation['parameters'][] = $parameter;
166
                        }
167
                    }
168
                }
169
170
                $paths[$this->getPath($subresourceOperation['shortNames'][0], $subresourceOperation['route_name'], $subresourceOperation, OperationType::SUBRESOURCE)] = new \ArrayObject(['get' => $pathOperation]);
171
            }
172
        }
173
174
        $definitions->ksort();
175
        $paths->ksort();
176
177
        return $this->computeDoc($object, $definitions, $paths, $context);
178
    }
179
180
    /**
181
     * Updates the list of entries in the paths collection.
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::COLLECTION === $operationType ? $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::ITEM === $operationType ? $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
    private function getPath(string $resourceShortName, string $operationName, array $operation, string $operationType): string
206
    {
207
        $path = $this->operationPathResolver->resolveOperationPath($resourceShortName, $operation, $operationType, $operationName);
208
        if ('.{_format}' === substr($path, -10)) {
209
            $path = substr($path, 0, -10);
210
        }
211
212
        return $path;
213
    }
214
215
    /**
216
     * Gets a path Operation Object.
217
     *
218
     * @see https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#operation-object
219
     *
220
     * @param string[] $mimeTypes
221
     */
222
    private function getPathOperation(string $operationName, array $operation, string $method, string $operationType, string $resourceClass, ResourceMetadata $resourceMetadata, array $mimeTypes, \ArrayObject $definitions): \ArrayObject
223
    {
224
        $pathOperation = new \ArrayObject($operation['swagger_context'] ?? []);
225
        $resourceShortName = $resourceMetadata->getShortName();
226
        $pathOperation['tags'] ?? $pathOperation['tags'] = [$resourceShortName];
227
        $pathOperation['operationId'] ?? $pathOperation['operationId'] = lcfirst($operationName).ucfirst($resourceShortName).ucfirst($operationType);
228
        if ($resourceMetadata->getTypedOperationAttribute($operationType, $operationName, 'deprecation_reason', null, true)) {
229
            $pathOperation['deprecated'] = true;
230
        }
231
232
        switch ($method) {
233
            case 'GET':
234
                return $this->updateGetOperation($pathOperation, $mimeTypes, $operationType, $resourceMetadata, $resourceClass, $resourceShortName, $operationName, $definitions);
235
            case 'POST':
236
                return $this->updatePostOperation($pathOperation, $mimeTypes, $operationType, $resourceMetadata, $resourceClass, $resourceShortName, $operationName, $definitions);
237
            case 'PATCH':
238
                $pathOperation['summary'] ?? $pathOperation['summary'] = sprintf('Updates the %s resource.', $resourceShortName);
239
                // no break
240
            case 'PUT':
241
                return $this->updatePutOperation($pathOperation, $mimeTypes, $operationType, $resourceMetadata, $resourceClass, $resourceShortName, $operationName, $definitions);
242
            case 'DELETE':
243
                return $this->updateDeleteOperation($pathOperation, $resourceShortName);
244
        }
245
246
        return $pathOperation;
247
    }
248
249
    /**
250
     * @return \ArrayObject
251
     */
252
    private function updateGetOperation(\ArrayObject $pathOperation, array $mimeTypes, string $operationType, ResourceMetadata $resourceMetadata, string $resourceClass, string $resourceShortName, string $operationName, \ArrayObject $definitions)
253
    {
254
        $serializerContext = $this->getSerializerContext($operationType, false, $resourceMetadata, $operationName);
255
        $responseDefinitionKey = $this->getDefinition($definitions, $resourceMetadata, $resourceClass, $serializerContext);
256
257
        $pathOperation['produces'] ?? $pathOperation['produces'] = $mimeTypes;
258
259
        if (OperationType::COLLECTION === $operationType) {
260
            $pathOperation['summary'] ?? $pathOperation['summary'] = sprintf('Retrieves the collection of %s resources.', $resourceShortName);
261
            $pathOperation['responses'] ?? $pathOperation['responses'] = [
262
                '200' => [
263
                    'description' => sprintf('%s collection response', $resourceShortName),
264
                    'schema' => [
265
                        'type' => 'array',
266
                        'items' => ['$ref' => sprintf('#/definitions/%s', $responseDefinitionKey)],
267
                    ],
268
                ],
269
            ];
270
271
            if (!isset($pathOperation['parameters']) && $parameters = $this->getFiltersParameters($resourceClass, $operationName, $resourceMetadata, $definitions, $serializerContext)) {
272
                $pathOperation['parameters'] = $parameters;
273
            }
274
275
            if ($this->paginationEnabled && $resourceMetadata->getCollectionOperationAttribute($operationName, 'pagination_enabled', true, true)) {
276
                $pathOperation['parameters'][] = $this->getPaginationParameters();
277
278
                if ($resourceMetadata->getCollectionOperationAttribute($operationName, 'pagination_client_items_per_page', $this->clientItemsPerPage, true)) {
279
                    $pathOperation['parameters'][] = $this->getItemsPerPageParameters();
280
                }
281
            }
282
            if ($this->paginationEnabled && $resourceMetadata->getCollectionOperationAttribute($operationName, 'pagination_client_enabled', $this->paginationClientEnabled, true)) {
283
                $pathOperation['parameters'][] = $this->getPaginationClientEnabledParameters();
284
            }
285
286
            return $pathOperation;
287
        }
288
289
        $pathOperation['summary'] ?? $pathOperation['summary'] = sprintf('Retrieves a %s resource.', $resourceShortName);
290
        $pathOperation['parameters'] ?? $pathOperation['parameters'] = [[
291
            'name' => 'id',
292
            'in' => 'path',
293
            'required' => true,
294
            'type' => 'string',
295
        ]];
296
        $pathOperation['responses'] ?? $pathOperation['responses'] = [
297
            '200' => [
298
                'description' => sprintf('%s resource response', $resourceShortName),
299
                'schema' => ['$ref' => sprintf('#/definitions/%s', $responseDefinitionKey)],
300
            ],
301
            '404' => ['description' => 'Resource not found'],
302
        ];
303
304
        return $pathOperation;
305
    }
306
307
    /**
308
     * @return \ArrayObject
309
     */
310
    private function updatePostOperation(\ArrayObject $pathOperation, array $mimeTypes, string $operationType, ResourceMetadata $resourceMetadata, string $resourceClass, string $resourceShortName, string $operationName, \ArrayObject $definitions)
311
    {
312
        $pathOperation['consumes'] ?? $pathOperation['consumes'] = $mimeTypes;
313
        $pathOperation['produces'] ?? $pathOperation['produces'] = $mimeTypes;
314
        $pathOperation['summary'] ?? $pathOperation['summary'] = sprintf('Creates a %s resource.', $resourceShortName);
315
        $pathOperation['parameters'] ?? $pathOperation['parameters'] = [[
316
            'name' => lcfirst($resourceShortName),
317
            'in' => 'body',
318
            'description' => sprintf('The new %s resource', $resourceShortName),
319
            'schema' => ['$ref' => sprintf('#/definitions/%s', $this->getDefinition($definitions, $resourceMetadata, $resourceClass,
320
                $this->getSerializerContext($operationType, true, $resourceMetadata, $operationName)
321
            ))],
322
        ]];
323
        $pathOperation['responses'] ?? $pathOperation['responses'] = [
324
            '201' => [
325
                'description' => sprintf('%s resource created', $resourceShortName),
326
                'schema' => ['$ref' => sprintf('#/definitions/%s', $this->getDefinition($definitions, $resourceMetadata, $resourceClass,
327
                    $this->getSerializerContext($operationType, false, $resourceMetadata, $operationName)
328
                ))],
329
            ],
330
            '400' => ['description' => 'Invalid input'],
331
            '404' => ['description' => 'Resource not found'],
332
        ];
333
334
        return $pathOperation;
335
    }
336
337
    /**
338
     * @return \ArrayObject
339
     */
340
    private function updatePutOperation(\ArrayObject $pathOperation, array $mimeTypes, string $operationType, ResourceMetadata $resourceMetadata, string $resourceClass, string $resourceShortName, string $operationName, \ArrayObject $definitions)
341
    {
342
        $pathOperation['consumes'] ?? $pathOperation['consumes'] = $mimeTypes;
343
        $pathOperation['produces'] ?? $pathOperation['produces'] = $mimeTypes;
344
        $pathOperation['summary'] ?? $pathOperation['summary'] = sprintf('Replaces the %s resource.', $resourceShortName);
345
        $pathOperation['parameters'] ?? $pathOperation['parameters'] = [
346
            [
347
                'name' => 'id',
348
                'in' => 'path',
349
                'type' => 'string',
350
                'required' => true,
351
            ],
352
            [
353
                'name' => lcfirst($resourceShortName),
354
                'in' => 'body',
355
                'description' => sprintf('The updated %s resource', $resourceShortName),
356
                'schema' => ['$ref' => sprintf('#/definitions/%s', $this->getDefinition($definitions, $resourceMetadata, $resourceClass,
357
                    $this->getSerializerContext($operationType, true, $resourceMetadata, $operationName)
358
                ))],
359
            ],
360
        ];
361
        $pathOperation['responses'] ?? $pathOperation['responses'] = [
362
            '200' => [
363
                'description' => sprintf('%s resource updated', $resourceShortName),
364
                'schema' => ['$ref' => sprintf('#/definitions/%s', $this->getDefinition($definitions, $resourceMetadata, $resourceClass,
365
                    $this->getSerializerContext($operationType, false, $resourceMetadata, $operationName)
366
                ))],
367
            ],
368
            '400' => ['description' => 'Invalid input'],
369
            '404' => ['description' => 'Resource not found'],
370
        ];
371
372
        return $pathOperation;
373
    }
374
375
    private function updateDeleteOperation(\ArrayObject $pathOperation, string $resourceShortName): \ArrayObject
376
    {
377
        $pathOperation['summary'] ?? $pathOperation['summary'] = sprintf('Removes the %s resource.', $resourceShortName);
378
        $pathOperation['responses'] ?? $pathOperation['responses'] = [
379
            '204' => ['description' => sprintf('%s resource deleted', $resourceShortName)],
380
            '404' => ['description' => 'Resource not found'],
381
        ];
382
383
        $pathOperation['parameters'] ?? $pathOperation['parameters'] = [[
384
            'name' => 'id',
385
            'in' => 'path',
386
            'type' => 'string',
387
            'required' => true,
388
        ]];
389
390
        return $pathOperation;
391
    }
392
393
    private function getDefinition(\ArrayObject $definitions, ResourceMetadata $resourceMetadata, string $resourceClass, array $serializerContext = null): string
394
    {
395
        if (isset($serializerContext[self::SWAGGER_DEFINITION_NAME])) {
396
            $definitionKey = sprintf('%s-%s', $resourceMetadata->getShortName(), $serializerContext[self::SWAGGER_DEFINITION_NAME]);
397
        } else {
398
            $definitionKey = $this->getDefinitionKey($resourceMetadata->getShortName(), (array) ($serializerContext[AbstractNormalizer::GROUPS] ?? []));
399
        }
400
401
        if (!isset($definitions[$definitionKey])) {
402
            $definitions[$definitionKey] = [];  // Initialize first to prevent infinite loop
403
            $definitions[$definitionKey] = $this->getDefinitionSchema($resourceClass, $resourceMetadata, $definitions, $serializerContext);
404
        }
405
406
        return $definitionKey;
407
    }
408
409
    private function getDefinitionKey(string $resourceShortName, array $groups): string
410
    {
411
        return $groups ? sprintf('%s-%s', $resourceShortName, implode('_', $groups)) : $resourceShortName;
412
    }
413
414
    /**
415
     * Gets a definition Schema Object.
416
     *
417
     * @see https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#schemaObject
418
     */
419
    private function getDefinitionSchema(string $resourceClass, ResourceMetadata $resourceMetadata, \ArrayObject $definitions, array $serializerContext = null): \ArrayObject
420
    {
421
        $definitionSchema = new \ArrayObject(['type' => 'object']);
422
423
        if (null !== $description = $resourceMetadata->getDescription()) {
424
            $definitionSchema['description'] = $description;
425
        }
426
427
        if (null !== $iri = $resourceMetadata->getIri()) {
428
            $definitionSchema['externalDocs'] = ['url' => $iri];
429
        }
430
431
        $options = isset($serializerContext[AbstractNormalizer::GROUPS]) ? ['serializer_groups' => $serializerContext[AbstractNormalizer::GROUPS]] : [];
432
        foreach ($this->propertyNameCollectionFactory->create($resourceClass, $options) as $propertyName) {
433
            $propertyMetadata = $this->propertyMetadataFactory->create($resourceClass, $propertyName);
434
            $normalizedPropertyName = $this->nameConverter ? $this->nameConverter->normalize($propertyName) : $propertyName;
435
436
            if ($propertyMetadata->isRequired()) {
437
                $definitionSchema['required'][] = $normalizedPropertyName;
438
            }
439
440
            $definitionSchema['properties'][$normalizedPropertyName] = $this->getPropertySchema($propertyMetadata, $definitions, $serializerContext);
441
        }
442
443
        return $definitionSchema;
444
    }
445
446
    /**
447
     * Gets a property Schema Object.
448
     *
449
     * @see https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#schemaObject
450
     */
451
    private function getPropertySchema(PropertyMetadata $propertyMetadata, \ArrayObject $definitions, array $serializerContext = null): \ArrayObject
452
    {
453
        $propertySchema = new \ArrayObject($propertyMetadata->getAttributes()['swagger_context'] ?? []);
454
455
        if (false === $propertyMetadata->isWritable()) {
456
            $propertySchema['readOnly'] = true;
457
        }
458
459
        if (null !== $description = $propertyMetadata->getDescription()) {
460
            $propertySchema['description'] = $description;
461
        }
462
463
        if (null === $type = $propertyMetadata->getType()) {
464
            return $propertySchema;
465
        }
466
467
        $isCollection = $type->isCollection();
468
        if (null === $valueType = $isCollection ? $type->getCollectionValueType() : $type) {
469
            $builtinType = 'string';
470
            $className = null;
471
        } else {
472
            $builtinType = $valueType->getBuiltinType();
473
            $className = $valueType->getClassName();
474
        }
475
476
        $valueSchema = $this->getType($builtinType, $isCollection, $className, $propertyMetadata->isReadableLink(), $definitions, $serializerContext);
477
478
        return new \ArrayObject((array) $propertySchema + $valueSchema);
479
    }
480
481
    /**
482
     * Gets the Swagger's type corresponding to the given PHP's type.
483
     *
484
     * @param string $className
485
     * @param bool   $readableLink
486
     */
487
    private function getType(string $type, bool $isCollection, string $className = null, bool $readableLink = null, \ArrayObject $definitions, array $serializerContext = null): array
488
    {
489
        if ($isCollection) {
490
            return ['type' => 'array', 'items' => $this->getType($type, false, $className, $readableLink, $definitions, $serializerContext)];
491
        }
492
493
        if (Type::BUILTIN_TYPE_STRING === $type) {
494
            return ['type' => 'string'];
495
        }
496
497
        if (Type::BUILTIN_TYPE_INT === $type) {
498
            return ['type' => 'integer'];
499
        }
500
501
        if (Type::BUILTIN_TYPE_FLOAT === $type) {
502
            return ['type' => 'number'];
503
        }
504
505
        if (Type::BUILTIN_TYPE_BOOL === $type) {
506
            return ['type' => 'boolean'];
507
        }
508
509
        if (Type::BUILTIN_TYPE_OBJECT === $type) {
510
            if (null === $className) {
511
                return ['type' => 'string'];
512
            }
513
514
            if (is_subclass_of($className, \DateTimeInterface::class)) {
515
                return ['type' => 'string', 'format' => 'date-time'];
516
            }
517
518
            if (!$this->resourceClassResolver->isResourceClass($className)) {
519
                return ['type' => 'string'];
520
            }
521
522
            if (true === $readableLink) {
523
                return ['$ref' => sprintf('#/definitions/%s', $this->getDefinition($definitions,
524
                    $this->resourceMetadataFactory->create($className),
525
                    $className, $serializerContext)
526
                )];
527
            }
528
        }
529
530
        return ['type' => 'string'];
531
    }
532
533
    /**
534
     * Computes the Swagger documentation.
535
     */
536
    private function computeDoc(Documentation $documentation, \ArrayObject $definitions, \ArrayObject $paths, array $context): array
537
    {
538
        $doc = [
539
            'swagger' => self::SWAGGER_VERSION,
540
            'basePath' => $context['base_url'] ?? '/',
541
            'info' => [
542
                'title' => $documentation->getTitle(),
543
                'version' => $documentation->getVersion(),
544
            ],
545
            'paths' => $paths,
546
        ];
547
548
        $securityDefinitions = [];
549
        $security = [];
550
551
        if ($this->oauthEnabled) {
552
            $securityDefinitions['oauth'] = [
553
                'type' => $this->oauthType,
554
                'description' => 'OAuth client_credentials Grant',
555
                'flow' => $this->oauthFlow,
556
                'tokenUrl' => $this->oauthTokenUrl,
557
                'authorizationUrl' => $this->oauthAuthorizationUrl,
558
                'scopes' => $this->oauthScopes,
559
            ];
560
561
            $security[] = ['oauth' => []];
562
        }
563
564
        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...
565
            foreach ($this->apiKeys as $key => $apiKey) {
566
                $name = $apiKey['name'];
567
                $type = $apiKey['type'];
568
569
                $securityDefinitions[$key] = [
570
                    'type' => 'apiKey',
571
                    'in' => $type,
572
                    'description' => sprintf('Value for the %s %s', $name, 'query' === $type ? sprintf('%s parameter', $type) : $type),
573
                    'name' => $name,
574
                ];
575
576
                $security[] = [$key => []];
577
            }
578
        }
579
580
        if ($securityDefinitions && $security) {
581
            $doc['securityDefinitions'] = $securityDefinitions;
582
            $doc['security'] = $security;
583
        }
584
585
        if ('' !== $description = $documentation->getDescription()) {
586
            $doc['info']['description'] = $description;
587
        }
588
589
        if (\count($definitions) > 0) {
590
            $doc['definitions'] = $definitions;
591
        }
592
593
        return $doc;
594
    }
595
596
    /**
597
     * Gets Swagger parameters corresponding to enabled filters.
598
     */
599
    private function getFiltersParameters(string $resourceClass, string $operationName, ResourceMetadata $resourceMetadata, \ArrayObject $definitions, array $serializerContext = null): array
600
    {
601
        if (null === $this->filterLocator) {
602
            return [];
603
        }
604
605
        $parameters = [];
606
        $resourceFilters = $resourceMetadata->getCollectionOperationAttribute($operationName, 'filters', [], true);
607
        foreach ($resourceFilters as $filterId) {
608
            if (!$filter = $this->getFilter($filterId)) {
609
                continue;
610
            }
611
612
            foreach ($filter->getDescription($resourceClass) as $name => $data) {
613
                $parameter = [
614
                    'name' => $name,
615
                    'in' => 'query',
616
                    'required' => $data['required'],
617
                ];
618
                $parameter += $this->getType($data['type'], false, null, null, $definitions, $serializerContext);
619
620
                if (isset($data['swagger'])) {
621
                    $parameter = $data['swagger'] + $parameter;
622
                }
623
624
                $parameters[] = $parameter;
625
            }
626
        }
627
628
        return $parameters;
629
    }
630
631
    /**
632
     * Returns pagination parameters for the "get" collection operation.
633
     */
634
    private function getPaginationParameters(): array
635
    {
636
        return [
637
            'name' => $this->paginationPageParameterName,
638
            'in' => 'query',
639
            'required' => false,
640
            'type' => 'integer',
641
            'description' => 'The collection page number',
642
        ];
643
    }
644
645
    /**
646
     * Returns enable pagination parameter for the "get" collection operation
647
     */
648
    private function getPaginationClientEnabledParameters(): array
649
    {
650
        return [
651
            'name' => $this->paginationClientEnabledParameterName,
652
            'in' => 'query',
653
            'required' => false,
654
            'type' => 'boolean',
655
            'description' => 'Enable or disable pagination',
656
        ];
657
    }
658
659
    /**
660
     * Returns items per page parameters for the "get" collection operation.
661
     */
662
    private function getItemsPerPageParameters(): array
663
    {
664
        return [
665
            'name' => $this->itemsPerPageParameterName,
666
            'in' => 'query',
667
            'required' => false,
668
            'type' => 'integer',
669
            'description' => 'The number of items per page',
670
        ];
671
    }
672
673
    /**
674
     * {@inheritdoc}
675
     */
676
    public function supportsNormalization($data, $format = null)
677
    {
678
        return self::FORMAT === $format && $data instanceof Documentation;
679
    }
680
681
    /**
682
     * {@inheritdoc}
683
     */
684
    public function hasCacheableSupportsMethod(): bool
685
    {
686
        return true;
687
    }
688
689
    /**
690
     * @return array|null
691
     */
692
    private function getSerializerContext(string $operationType, bool $denormalization, ResourceMetadata $resourceMetadata, string $operationName)
693
    {
694
        $contextKey = $denormalization ? 'denormalization_context' : 'normalization_context';
695
696
        if (OperationType::COLLECTION === $operationType) {
697
            return $resourceMetadata->getCollectionOperationAttribute($operationName, $contextKey, null, true);
698
        }
699
700
        return $resourceMetadata->getItemOperationAttribute($operationName, $contextKey, null, true);
701
    }
702
}
703