Passed
Push — master ( dcc973...7e1d63 )
by Kévin
03:09
created

DocumentationNormalizer::getPathOperation()   C

Complexity

Conditions 7
Paths 16

Size

Total Lines 25
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 25
rs 6.7272
c 0
b 0
f 0
cc 7
eloc 18
nc 16
nop 8

How to fix   Many Parameters   

Many Parameters

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

There are several approaches to avoid long parameter lists:

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