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

DocumentationNormalizer::hasCacheableSupportsMethod()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

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