DocumentationNormalizer::computeDoc()   F
last analyzed

Complexity

Conditions 19
Paths 1008

Size

Total Lines 96
Code Lines 60

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 19
eloc 60
nc 1008
nop 5
dl 0
loc 96
rs 0.3499
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
/*
4
 * This file is part of the API Platform project.
5
 *
6
 * (c) Kévin Dunglas <[email protected]>
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 */
11
12
declare(strict_types=1);
13
14
namespace ApiPlatform\Core\Swagger\Serializer;
15
16
use ApiPlatform\Core\Api\FilterCollection;
17
use ApiPlatform\Core\Api\FilterLocatorTrait;
18
use ApiPlatform\Core\Api\FormatsProviderInterface;
19
use ApiPlatform\Core\Api\OperationAwareFormatsProviderInterface;
20
use ApiPlatform\Core\Api\OperationMethodResolverInterface;
21
use ApiPlatform\Core\Api\OperationType;
22
use ApiPlatform\Core\Api\ResourceClassResolverInterface;
23
use ApiPlatform\Core\Api\UrlGeneratorInterface;
24
use ApiPlatform\Core\Documentation\Documentation;
25
use ApiPlatform\Core\Exception\ResourceClassNotFoundException;
26
use ApiPlatform\Core\JsonSchema\Schema;
27
use ApiPlatform\Core\JsonSchema\SchemaFactory;
28
use ApiPlatform\Core\JsonSchema\SchemaFactoryInterface;
29
use ApiPlatform\Core\JsonSchema\TypeFactory;
30
use ApiPlatform\Core\JsonSchema\TypeFactoryInterface;
31
use ApiPlatform\Core\Metadata\Property\Factory\PropertyMetadataFactoryInterface;
32
use ApiPlatform\Core\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface;
33
use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface;
34
use ApiPlatform\Core\Metadata\Resource\ResourceMetadata;
35
use ApiPlatform\Core\Operation\Factory\SubresourceOperationFactoryInterface;
36
use ApiPlatform\Core\PathResolver\OperationPathResolverInterface;
37
use Psr\Container\ContainerInterface;
38
use Symfony\Component\PropertyInfo\Type;
39
use Symfony\Component\Serializer\NameConverter\NameConverterInterface;
40
use Symfony\Component\Serializer\Normalizer\CacheableSupportsMethodInterface;
41
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
42
43
/**
44
 * Generates an OpenAPI specification (formerly known as Swagger). OpenAPI v2 and v3 are supported.
45
 *
46
 * @author Amrouche Hamza <[email protected]>
47
 * @author Teoh Han Hui <[email protected]>
48
 * @author Kévin Dunglas <[email protected]>
49
 * @author Anthony GRASSIOT <[email protected]>
50
 */
51
final class DocumentationNormalizer implements NormalizerInterface, CacheableSupportsMethodInterface
52
{
53
    use FilterLocatorTrait;
54
55
    public const FORMAT = 'json';
56
    public const BASE_URL = 'base_url';
57
    public const SPEC_VERSION = 'spec_version';
58
    public const OPENAPI_VERSION = '3.0.2';
59
    public const SWAGGER_DEFINITION_NAME = 'swagger_definition_name';
60
    public const SWAGGER_VERSION = '2.0';
61
62
    /**
63
     * @deprecated
64
     */
65
    public const ATTRIBUTE_NAME = 'swagger_context';
66
67
    private $resourceMetadataFactory;
68
    private $propertyNameCollectionFactory;
69
    private $propertyMetadataFactory;
70
    private $operationMethodResolver;
71
    private $operationPathResolver;
72
    private $oauthEnabled;
73
    private $oauthType;
74
    private $oauthFlow;
75
    private $oauthTokenUrl;
76
    private $oauthAuthorizationUrl;
77
    private $oauthScopes;
78
    private $apiKeys;
79
    private $subresourceOperationFactory;
80
    private $paginationEnabled;
81
    private $paginationPageParameterName;
82
    private $clientItemsPerPage;
83
    private $itemsPerPageParameterName;
84
    private $paginationClientEnabled;
85
    private $paginationClientEnabledParameterName;
86
    private $formats;
87
    private $formatsProvider;
88
    /**
89
     * @var SchemaFactoryInterface
90
     */
91
    private $jsonSchemaFactory;
92
    /**
93
     * @var TypeFactoryInterface
94
     */
95
    private $jsonSchemaTypeFactory;
96
    private $defaultContext = [
97
        self::BASE_URL => '/',
98
        ApiGatewayNormalizer::API_GATEWAY => false,
99
    ];
100
101
    /**
102
     * @param SchemaFactoryInterface|ResourceClassResolverInterface|null $jsonSchemaFactory
103
     * @param ContainerInterface|FilterCollection|null                   $filterLocator
104
     * @param array|OperationAwareFormatsProviderInterface               $formats
105
     * @param mixed|null                                                 $jsonSchemaTypeFactory
106
     * @param int[]                                                      $swaggerVersions
107
     */
108
    public function __construct(ResourceMetadataFactoryInterface $resourceMetadataFactory, PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, $jsonSchemaFactory = null, $jsonSchemaTypeFactory = null, 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', $formats = [], bool $paginationClientEnabled = false, string $paginationClientEnabledParameterName = 'pagination', array $defaultContext = [], array $swaggerVersions = [2, 3])
109
    {
110
        if ($jsonSchemaTypeFactory instanceof OperationMethodResolverInterface) {
111
            @trigger_error(sprintf('Passing an instance of %s to %s() is deprecated since version 2.5 and will be removed in 3.0.', OperationMethodResolverInterface::class, __METHOD__), E_USER_DEPRECATED);
112
113
            $this->operationMethodResolver = $jsonSchemaTypeFactory;
114
            $this->jsonSchemaTypeFactory = new TypeFactory();
115
        } else {
116
            $this->jsonSchemaTypeFactory = $jsonSchemaTypeFactory ?? new TypeFactory();
117
        }
118
119
        if ($jsonSchemaFactory instanceof ResourceClassResolverInterface) {
120
            @trigger_error(sprintf('Passing an instance of %s to %s() is deprecated since version 2.5 and will be removed in 3.0.', ResourceClassResolverInterface::class, __METHOD__), E_USER_DEPRECATED);
121
        }
122
123
        if (null === $jsonSchemaFactory || $jsonSchemaFactory instanceof ResourceClassResolverInterface) {
124
            $jsonSchemaFactory = new SchemaFactory($this->jsonSchemaTypeFactory, $resourceMetadataFactory, $propertyNameCollectionFactory, $propertyMetadataFactory, $nameConverter);
125
            $this->jsonSchemaTypeFactory->setSchemaFactory($jsonSchemaFactory);
126
        }
127
        $this->jsonSchemaFactory = $jsonSchemaFactory;
128
129
        if ($nameConverter) {
130
            @trigger_error(sprintf('Passing an instance of %s to %s() is deprecated since version 2.5 and will be removed in 3.0.', NameConverterInterface::class, __METHOD__), E_USER_DEPRECATED);
131
        }
132
133
        if ($urlGenerator) {
134
            @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);
135
        }
136
137
        if ($formats instanceof FormatsProviderInterface) {
138
            @trigger_error(sprintf('Passing an instance of %s to %s() is deprecated since version 2.5 and will be removed in 3.0, pass an array instead.', FormatsProviderInterface::class, __METHOD__), E_USER_DEPRECATED);
139
140
            $this->formatsProvider = $formats;
141
        } else {
142
            $this->formats = $formats;
143
        }
144
145
        $this->setFilterLocator($filterLocator, true);
146
147
        $this->resourceMetadataFactory = $resourceMetadataFactory;
148
        $this->propertyNameCollectionFactory = $propertyNameCollectionFactory;
149
        $this->propertyMetadataFactory = $propertyMetadataFactory;
150
        $this->operationPathResolver = $operationPathResolver;
151
        $this->oauthEnabled = $oauthEnabled;
152
        $this->oauthType = $oauthType;
153
        $this->oauthFlow = $oauthFlow;
154
        $this->oauthTokenUrl = $oauthTokenUrl;
155
        $this->oauthAuthorizationUrl = $oauthAuthorizationUrl;
156
        $this->oauthScopes = $oauthScopes;
157
        $this->subresourceOperationFactory = $subresourceOperationFactory;
158
        $this->paginationEnabled = $paginationEnabled;
159
        $this->paginationPageParameterName = $paginationPageParameterName;
160
        $this->apiKeys = $apiKeys;
161
        $this->clientItemsPerPage = $clientItemsPerPage;
162
        $this->itemsPerPageParameterName = $itemsPerPageParameterName;
163
        $this->paginationClientEnabled = $paginationClientEnabled;
164
        $this->paginationClientEnabledParameterName = $paginationClientEnabledParameterName;
165
166
        $this->defaultContext[self::SPEC_VERSION] = $swaggerVersions[0] ?? 2;
167
168
        $this->defaultContext = array_merge($this->defaultContext, $defaultContext);
169
    }
170
171
    /**
172
     * {@inheritdoc}
173
     */
174
    public function normalize($object, $format = null, array $context = [])
175
    {
176
        $v3 = 3 === ($context['spec_version'] ?? $this->defaultContext['spec_version']) && !($context['api_gateway'] ?? $this->defaultContext['api_gateway']);
177
178
        $definitions = new \ArrayObject();
179
        $paths = new \ArrayObject();
180
        $links = new \ArrayObject();
181
182
        foreach ($object->getResourceNameCollection() as $resourceClass) {
183
            $resourceMetadata = $this->resourceMetadataFactory->create($resourceClass);
184
            $resourceShortName = $resourceMetadata->getShortName();
185
186
            // Items needs to be parsed first to be able to reference the lines from the collection operation
187
            $this->addPaths($v3, $paths, $definitions, $resourceClass, $resourceShortName, $resourceMetadata, OperationType::ITEM, $links);
0 ignored issues
show
Bug introduced by
It seems like $resourceShortName can also be of type null; however, parameter $resourceShortName of ApiPlatform\Core\Swagger...nNormalizer::addPaths() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

187
            $this->addPaths($v3, $paths, $definitions, $resourceClass, /** @scrutinizer ignore-type */ $resourceShortName, $resourceMetadata, OperationType::ITEM, $links);
Loading history...
188
            $this->addPaths($v3, $paths, $definitions, $resourceClass, $resourceShortName, $resourceMetadata, OperationType::COLLECTION, $links);
189
190
            if (null === $this->subresourceOperationFactory) {
191
                continue;
192
            }
193
194
            foreach ($this->subresourceOperationFactory->create($resourceClass) as $operationId => $subresourceOperation) {
195
                $paths[$this->getPath($subresourceOperation['shortNames'][0], $subresourceOperation['route_name'], $subresourceOperation, OperationType::SUBRESOURCE)] = $this->addSubresourceOperation($v3, $subresourceOperation, $definitions, $operationId, $resourceMetadata);
196
            }
197
        }
198
199
        $definitions->ksort();
200
        $paths->ksort();
201
202
        return $this->computeDoc($v3, $object, $definitions, $paths, $context);
203
    }
204
205
    /**
206
     * Updates the list of entries in the paths collection.
207
     */
208
    private function addPaths(bool $v3, \ArrayObject $paths, \ArrayObject $definitions, string $resourceClass, string $resourceShortName, ResourceMetadata $resourceMetadata, string $operationType, \ArrayObject $links)
209
    {
210
        if (null === $operations = OperationType::COLLECTION === $operationType ? $resourceMetadata->getCollectionOperations() : $resourceMetadata->getItemOperations()) {
211
            return;
212
        }
213
214
        foreach ($operations as $operationName => $operation) {
215
            $path = $this->getPath($resourceShortName, $operationName, $operation, $operationType);
216
            if ($this->operationMethodResolver) {
217
                $method = OperationType::ITEM === $operationType ? $this->operationMethodResolver->getItemOperationMethod($resourceClass, $operationName) : $this->operationMethodResolver->getCollectionOperationMethod($resourceClass, $operationName);
218
            } else {
219
                $method = $resourceMetadata->getTypedOperationAttribute($operationType, $operationName, 'method', 'GET');
220
            }
221
222
            $paths[$path][strtolower($method)] = $this->getPathOperation($v3, $operationName, $operation, $method, $operationType, $resourceClass, $resourceMetadata, $definitions, $links);
223
        }
224
    }
225
226
    /**
227
     * Gets the path for an operation.
228
     *
229
     * If the path ends with the optional _format parameter, it is removed
230
     * as optional path parameters are not yet supported.
231
     *
232
     * @see https://github.com/OAI/OpenAPI-Specification/issues/93
233
     */
234
    private function getPath(string $resourceShortName, string $operationName, array $operation, string $operationType): string
235
    {
236
        $path = $this->operationPathResolver->resolveOperationPath($resourceShortName, $operation, $operationType, $operationName);
237
        if ('.{_format}' === substr($path, -10)) {
238
            $path = substr($path, 0, -10);
239
        }
240
241
        return $path;
242
    }
243
244
    /**
245
     * Gets a path Operation Object.
246
     *
247
     * @see https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#operation-object
248
     */
249
    private function getPathOperation(bool $v3, string $operationName, array $operation, string $method, string $operationType, string $resourceClass, ResourceMetadata $resourceMetadata, \ArrayObject $definitions, \ArrayObject $links): \ArrayObject
250
    {
251
        $pathOperation = new \ArrayObject($operation[$v3 ? 'openapi_context' : 'swagger_context'] ?? []);
252
        $resourceShortName = $resourceMetadata->getShortName();
253
        $pathOperation['tags'] ?? $pathOperation['tags'] = [$resourceShortName];
254
        $pathOperation['operationId'] ?? $pathOperation['operationId'] = lcfirst($operationName).ucfirst($resourceShortName).ucfirst($operationType);
255
        if ($v3 && 'GET' === $method && OperationType::ITEM === $operationType && $link = $this->getLinkObject($resourceClass, $pathOperation['operationId'], $this->getPath($resourceShortName, $operationName, $operation, $operationType))) {
0 ignored issues
show
Bug introduced by
It seems like $resourceShortName can also be of type null; however, parameter $resourceShortName of ApiPlatform\Core\Swagger...onNormalizer::getPath() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

255
        if ($v3 && 'GET' === $method && OperationType::ITEM === $operationType && $link = $this->getLinkObject($resourceClass, $pathOperation['operationId'], $this->getPath(/** @scrutinizer ignore-type */ $resourceShortName, $operationName, $operation, $operationType))) {
Loading history...
256
            $links[$pathOperation['operationId']] = $link;
257
        }
258
        if ($resourceMetadata->getTypedOperationAttribute($operationType, $operationName, 'deprecation_reason', null, true)) {
259
            $pathOperation['deprecated'] = true;
260
        }
261
262
        if (null === $this->formatsProvider) {
263
            $requestFormats = $resourceMetadata->getTypedOperationAttribute($operationType, $operationName, 'input_formats', [], true);
264
            $responseFormats = $resourceMetadata->getTypedOperationAttribute($operationType, $operationName, 'output_formats', [], true);
265
        } else {
266
            $requestFormats = $responseFormats = $this->formatsProvider->getFormatsFromOperation($resourceClass, $operationName, $operationType);
267
        }
268
269
        $requestMimeTypes = $this->flattenMimeTypes($requestFormats);
270
        $responseMimeTypes = $this->flattenMimeTypes($responseFormats);
271
        switch ($method) {
272
            case 'GET':
273
                return $this->updateGetOperation($v3, $pathOperation, $responseMimeTypes, $operationType, $resourceMetadata, $resourceClass, $resourceShortName, $operationName, $definitions);
0 ignored issues
show
Bug introduced by
It seems like $resourceShortName can also be of type null; however, parameter $resourceShortName of ApiPlatform\Core\Swagger...r::updateGetOperation() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

273
                return $this->updateGetOperation($v3, $pathOperation, $responseMimeTypes, $operationType, $resourceMetadata, $resourceClass, /** @scrutinizer ignore-type */ $resourceShortName, $operationName, $definitions);
Loading history...
274
            case 'POST':
275
                return $this->updatePostOperation($v3, $pathOperation, $requestMimeTypes, $responseMimeTypes, $operationType, $resourceMetadata, $resourceClass, $resourceShortName, $operationName, $definitions, $links);
0 ignored issues
show
Bug introduced by
It seems like $resourceShortName can also be of type null; however, parameter $resourceShortName of ApiPlatform\Core\Swagger...::updatePostOperation() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

275
                return $this->updatePostOperation($v3, $pathOperation, $requestMimeTypes, $responseMimeTypes, $operationType, $resourceMetadata, $resourceClass, /** @scrutinizer ignore-type */ $resourceShortName, $operationName, $definitions, $links);
Loading history...
276
            case 'PATCH':
277
                $pathOperation['summary'] ?? $pathOperation['summary'] = sprintf('Updates the %s resource.', $resourceShortName);
278
            // no break
279
            case 'PUT':
280
                return $this->updatePutOperation($v3, $pathOperation, $requestMimeTypes, $responseMimeTypes, $operationType, $resourceMetadata, $resourceClass, $resourceShortName, $operationName, $definitions);
0 ignored issues
show
Bug introduced by
It seems like $resourceShortName can also be of type null; however, parameter $resourceShortName of ApiPlatform\Core\Swagger...r::updatePutOperation() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

280
                return $this->updatePutOperation($v3, $pathOperation, $requestMimeTypes, $responseMimeTypes, $operationType, $resourceMetadata, $resourceClass, /** @scrutinizer ignore-type */ $resourceShortName, $operationName, $definitions);
Loading history...
281
            case 'DELETE':
282
                return $this->updateDeleteOperation($v3, $pathOperation, $resourceShortName, $operationType, $operationName, $resourceMetadata);
0 ignored issues
show
Bug introduced by
It seems like $resourceShortName can also be of type null; however, parameter $resourceShortName of ApiPlatform\Core\Swagger...updateDeleteOperation() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

282
                return $this->updateDeleteOperation($v3, $pathOperation, /** @scrutinizer ignore-type */ $resourceShortName, $operationType, $operationName, $resourceMetadata);
Loading history...
283
        }
284
285
        return $pathOperation;
286
    }
287
288
    /**
289
     * @return array the update message as first value, and if the schema is defined as second
290
     */
291
    private function addSchemas(bool $v3, array $message, \ArrayObject $definitions, string $resourceClass, string $operationType, string $operationName, array $mimeTypes, string $type = Schema::TYPE_OUTPUT, bool $forceCollection = false): array
292
    {
293
        if (!$v3) {
294
            $jsonSchema = $this->getJsonSchema($v3, $definitions, $resourceClass, $type, $operationType, $operationName, 'json', null, $forceCollection);
295
            if (!$jsonSchema->isDefined()) {
296
                return [$message, false];
297
            }
298
299
            $message['schema'] = $jsonSchema->getArrayCopy(false);
300
301
            return [$message, true];
302
        }
303
304
        foreach ($mimeTypes as $mimeType => $format) {
305
            $jsonSchema = $this->getJsonSchema($v3, $definitions, $resourceClass, $type, $operationType, $operationName, $format, null, $forceCollection);
306
            if (!$jsonSchema->isDefined()) {
307
                return [$message, false];
308
            }
309
310
            $message['content'][$mimeType] = ['schema' => $jsonSchema->getArrayCopy(false)];
311
        }
312
313
        return [$message, true];
314
    }
315
316
    private function updateGetOperation(bool $v3, \ArrayObject $pathOperation, array $mimeTypes, string $operationType, ResourceMetadata $resourceMetadata, string $resourceClass, string $resourceShortName, string $operationName, \ArrayObject $definitions): \ArrayObject
317
    {
318
        $successStatus = (string) $resourceMetadata->getTypedOperationAttribute($operationType, $operationName, 'status', '200');
319
320
        if (!$v3) {
321
            $pathOperation['produces'] ?? $pathOperation['produces'] = array_keys($mimeTypes);
322
        }
323
324
        if (OperationType::COLLECTION === $operationType) {
325
            $pathOperation['summary'] ?? $pathOperation['summary'] = sprintf('Retrieves the collection of %s resources.', $resourceShortName);
326
327
            $successResponse = ['description' => sprintf('%s collection response', $resourceShortName)];
328
            [$successResponse] = $this->addSchemas($v3, $successResponse, $definitions, $resourceClass, $operationType, $operationName, $mimeTypes);
329
330
            $pathOperation['responses'] ?? $pathOperation['responses'] = [$successStatus => $successResponse];
331
            $pathOperation['parameters'] ?? $pathOperation['parameters'] = $this->getFiltersParameters($v3, $resourceClass, $operationName, $resourceMetadata);
332
333
            $this->addPaginationParameters($v3, $resourceMetadata, $operationName, $pathOperation);
334
335
            return $pathOperation;
336
        }
337
338
        $pathOperation['summary'] ?? $pathOperation['summary'] = sprintf('Retrieves a %s resource.', $resourceShortName);
339
340
        $pathOperation = $this->addItemOperationParameters($v3, $pathOperation);
341
342
        $successResponse = ['description' => sprintf('%s resource response', $resourceShortName)];
343
        [$successResponse] = $this->addSchemas($v3, $successResponse, $definitions, $resourceClass, $operationType, $operationName, $mimeTypes);
344
345
        $pathOperation['responses'] ?? $pathOperation['responses'] = [
346
            $successStatus => $successResponse,
347
            '404' => ['description' => 'Resource not found'],
348
        ];
349
350
        return $pathOperation;
351
    }
352
353
    private function addPaginationParameters(bool $v3, ResourceMetadata $resourceMetadata, string $operationName, \ArrayObject $pathOperation)
354
    {
355
        if ($this->paginationEnabled && $resourceMetadata->getCollectionOperationAttribute($operationName, 'pagination_enabled', true, true)) {
356
            $paginationParameter = [
357
                'name' => $this->paginationPageParameterName,
358
                'in' => 'query',
359
                'required' => false,
360
                'description' => 'The collection page number',
361
            ];
362
            $v3 ? $paginationParameter['schema'] = [
363
                'type' => 'integer',
364
                'default' => 1,
365
            ] : $paginationParameter['type'] = 'integer';
366
            $pathOperation['parameters'][] = $paginationParameter;
367
368
            if ($resourceMetadata->getCollectionOperationAttribute($operationName, 'pagination_client_items_per_page', $this->clientItemsPerPage, true)) {
369
                $itemPerPageParameter = [
370
                    'name' => $this->itemsPerPageParameterName,
371
                    'in' => 'query',
372
                    'required' => false,
373
                    'description' => 'The number of items per page',
374
                ];
375
                if ($v3) {
376
                    $itemPerPageParameter['schema'] = [
377
                        'type' => 'integer',
378
                        'default' => $resourceMetadata->getCollectionOperationAttribute($operationName, 'pagination_items_per_page', 30, true),
379
                        'minimum' => 0,
380
                    ];
381
382
                    $maxItemsPerPage = $resourceMetadata->getCollectionOperationAttribute($operationName, 'maximum_items_per_page', null, true);
383
                    if (null !== $maxItemsPerPage) {
384
                        @trigger_error('The "maximum_items_per_page" option has been deprecated since API Platform 2.5 in favor of "pagination_maximum_items_per_page" and will be removed in API Platform 3.', E_USER_DEPRECATED);
385
                    }
386
                    $maxItemsPerPage = $resourceMetadata->getCollectionOperationAttribute($operationName, 'pagination_maximum_items_per_page', $maxItemsPerPage, true);
387
388
                    if (null !== $maxItemsPerPage) {
389
                        $itemPerPageParameter['schema']['maximum'] = $maxItemsPerPage;
390
                    }
391
                } else {
392
                    $itemPerPageParameter['type'] = 'integer';
393
                }
394
395
                $pathOperation['parameters'][] = $itemPerPageParameter;
396
            }
397
        }
398
399
        if ($this->paginationEnabled && $resourceMetadata->getCollectionOperationAttribute($operationName, 'pagination_client_enabled', $this->paginationClientEnabled, true)) {
400
            $paginationEnabledParameter = [
401
                'name' => $this->paginationClientEnabledParameterName,
402
                'in' => 'query',
403
                'required' => false,
404
                'description' => 'Enable or disable pagination',
405
            ];
406
            $v3 ? $paginationEnabledParameter['schema'] = ['type' => 'boolean'] : $paginationEnabledParameter['type'] = 'boolean';
407
            $pathOperation['parameters'][] = $paginationEnabledParameter;
408
        }
409
    }
410
411
    /**
412
     * @throws ResourceClassNotFoundException
413
     */
414
    private function addSubresourceOperation(bool $v3, array $subresourceOperation, \ArrayObject $definitions, string $operationId, ResourceMetadata $resourceMetadata): \ArrayObject
415
    {
416
        $operationName = 'get'; // TODO: we might want to extract that at some point to also support other subresource operations
417
        $collection = $subresourceOperation['collection'] ?? false;
418
419
        $subResourceMetadata = $this->resourceMetadataFactory->create($subresourceOperation['resource_class']);
420
421
        $pathOperation = new \ArrayObject([]);
422
        $pathOperation['tags'] = $subresourceOperation['shortNames'];
423
        $pathOperation['operationId'] = $operationId;
424
        $pathOperation['summary'] = sprintf('Retrieves %s%s resource%s.', $subresourceOperation['collection'] ? 'the collection of ' : 'a ', $subresourceOperation['shortNames'][0], $subresourceOperation['collection'] ? 's' : '');
425
426
        if (null === $this->formatsProvider) {
427
            // TODO: Subresource operation metadata aren't available by default, for now we have to fallback on default formats.
428
            // TODO: A better approach would be to always populate the subresource operation array.
429
            $responseFormats = $this
430
                ->resourceMetadataFactory
431
                ->create($subresourceOperation['resource_class'])
432
                ->getTypedOperationAttribute(OperationType::SUBRESOURCE, $operationName, 'output_formats', $this->formats, true);
433
        } else {
434
            $responseFormats = $this->formatsProvider->getFormatsFromOperation($subresourceOperation['resource_class'], $operationName, OperationType::SUBRESOURCE);
435
        }
436
437
        $mimeTypes = $this->flattenMimeTypes($responseFormats);
438
439
        if (!$v3) {
440
            $pathOperation['produces'] = array_keys($mimeTypes);
441
        }
442
443
        $successResponse = [
444
            'description' => sprintf('%s %s response', $subresourceOperation['shortNames'][0], $collection ? 'collection' : 'resource'),
445
        ];
446
        [$successResponse] = $this->addSchemas($v3, $successResponse, $definitions, $subresourceOperation['resource_class'], OperationType::SUBRESOURCE, $operationName, $mimeTypes, Schema::TYPE_OUTPUT, $collection);
447
448
        $pathOperation['responses'] = ['200' => $successResponse, '404' => ['description' => 'Resource not found']];
449
450
        // Avoid duplicates parameters when there is a filter on a subresource identifier
451
        $parametersMemory = [];
452
        $pathOperation['parameters'] = [];
453
        foreach ($subresourceOperation['identifiers'] as list($identifier, , $hasIdentifier)) {
454
            if (true === $hasIdentifier) {
455
                $parameter = ['name' => $identifier, 'in' => 'path', 'required' => true];
456
                $v3 ? $parameter['schema'] = ['type' => 'string'] : $parameter['type'] = 'string';
457
                $pathOperation['parameters'][] = $parameter;
458
                $parametersMemory[] = $identifier;
459
            }
460
        }
461
        if ($parameters = $this->getFiltersParameters($v3, $subresourceOperation['resource_class'], $operationName, $subResourceMetadata)) {
462
            foreach ($parameters as $parameter) {
463
                if (!\in_array($parameter['name'], $parametersMemory, true)) {
464
                    $pathOperation['parameters'][] = $parameter;
465
                }
466
            }
467
        }
468
469
        if ($subresourceOperation['collection']) {
470
            $this->addPaginationParameters($v3, $resourceMetadata, $operationName, $pathOperation);
471
        }
472
473
        return new \ArrayObject(['get' => $pathOperation]);
474
    }
475
476
    private function updatePostOperation(bool $v3, \ArrayObject $pathOperation, array $requestMimeTypes, array $responseMimeTypes, string $operationType, ResourceMetadata $resourceMetadata, string $resourceClass, string $resourceShortName, string $operationName, \ArrayObject $definitions, \ArrayObject $links): \ArrayObject
477
    {
478
        if (!$v3) {
479
            $pathOperation['consumes'] ?? $pathOperation['consumes'] = array_keys($requestMimeTypes);
480
            $pathOperation['produces'] ?? $pathOperation['produces'] = array_keys($responseMimeTypes);
481
        }
482
483
        $pathOperation['summary'] ?? $pathOperation['summary'] = sprintf('Creates a %s resource.', $resourceShortName);
484
485
        if (OperationType::ITEM === $operationType) {
486
            $pathOperation = $this->addItemOperationParameters($v3, $pathOperation);
487
        }
488
489
        $successResponse = ['description' => sprintf('%s resource created', $resourceShortName)];
490
        [$successResponse, $defined] = $this->addSchemas($v3, $successResponse, $definitions, $resourceClass, $operationType, $operationName, $responseMimeTypes);
491
492
        if ($defined && $v3 && ($links[$key = 'get'.ucfirst($resourceShortName).ucfirst(OperationType::ITEM)] ?? null)) {
493
            $successResponse['links'] = [ucfirst($key) => $links[$key]];
494
        }
495
496
        $pathOperation['responses'] ?? $pathOperation['responses'] = [
497
            (string) $resourceMetadata->getTypedOperationAttribute($operationType, $operationName, 'status', '201') => $successResponse,
498
            '400' => ['description' => 'Invalid input'],
499
            '404' => ['description' => 'Resource not found'],
500
        ];
501
502
        return $this->addRequestBody($v3, $pathOperation, $definitions, $resourceClass, $resourceShortName, $operationType, $operationName, $requestMimeTypes);
503
    }
504
505
    private function updatePutOperation(bool $v3, \ArrayObject $pathOperation, array $requestMimeTypes, array $responseMimeTypes, string $operationType, ResourceMetadata $resourceMetadata, string $resourceClass, string $resourceShortName, string $operationName, \ArrayObject $definitions): \ArrayObject
506
    {
507
        if (!$v3) {
508
            $pathOperation['consumes'] ?? $pathOperation['consumes'] = array_keys($requestMimeTypes);
509
            $pathOperation['produces'] ?? $pathOperation['produces'] = array_keys($responseMimeTypes);
510
        }
511
512
        $pathOperation['summary'] ?? $pathOperation['summary'] = sprintf('Replaces the %s resource.', $resourceShortName);
513
514
        $pathOperation = $this->addItemOperationParameters($v3, $pathOperation);
515
516
        $successResponse = ['description' => sprintf('%s resource updated', $resourceShortName)];
517
        [$successResponse] = $this->addSchemas($v3, $successResponse, $definitions, $resourceClass, $operationType, $operationName, $responseMimeTypes);
518
519
        $pathOperation['responses'] ?? $pathOperation['responses'] = [
520
            (string) $resourceMetadata->getTypedOperationAttribute($operationType, $operationName, 'status', '200') => $successResponse,
521
            '400' => ['description' => 'Invalid input'],
522
            '404' => ['description' => 'Resource not found'],
523
        ];
524
525
        return $this->addRequestBody($v3, $pathOperation, $definitions, $resourceClass, $resourceShortName, $operationType, $operationName, $requestMimeTypes, true);
526
    }
527
528
    private function addRequestBody(bool $v3, \ArrayObject $pathOperation, \ArrayObject $definitions, string $resourceClass, string $resourceShortName, string $operationType, string $operationName, array $requestMimeTypes, bool $put = false)
529
    {
530
        if (isset($pathOperation['requestBody'])) {
531
            return $pathOperation;
532
        }
533
534
        [$message, $defined] = $this->addSchemas($v3, [], $definitions, $resourceClass, $operationType, $operationName, $requestMimeTypes, Schema::TYPE_INPUT);
535
        if (!$defined) {
536
            return $pathOperation;
537
        }
538
539
        $description = sprintf('The %s %s resource', $put ? 'updated' : 'new', $resourceShortName);
540
        if ($v3) {
541
            $pathOperation['requestBody'] = $message + ['description' => $description];
542
543
            return $pathOperation;
544
        }
545
546
        if (!$this->hasBodyParameter($pathOperation['parameters'] ?? [])) {
547
            $pathOperation['parameters'][] = [
548
                'name' => lcfirst($resourceShortName),
549
                'in' => 'body',
550
                'description' => $description,
551
            ] + $message;
552
        }
553
554
        return $pathOperation;
555
    }
556
557
    private function hasBodyParameter(array $parameters): bool
558
    {
559
        foreach ($parameters as $parameter) {
560
            if (\array_key_exists('in', $parameter) && 'body' === $parameter['in']) {
561
                return true;
562
            }
563
        }
564
565
        return false;
566
    }
567
568
    private function updateDeleteOperation(bool $v3, \ArrayObject $pathOperation, string $resourceShortName, string $operationType, string $operationName, ResourceMetadata $resourceMetadata): \ArrayObject
569
    {
570
        $pathOperation['summary'] ?? $pathOperation['summary'] = sprintf('Removes the %s resource.', $resourceShortName);
571
        $pathOperation['responses'] ?? $pathOperation['responses'] = [
572
            (string) $resourceMetadata->getTypedOperationAttribute($operationType, $operationName, 'status', '204') => ['description' => sprintf('%s resource deleted', $resourceShortName)],
573
            '404' => ['description' => 'Resource not found'],
574
        ];
575
576
        return $this->addItemOperationParameters($v3, $pathOperation);
577
    }
578
579
    private function addItemOperationParameters(bool $v3, \ArrayObject $pathOperation): \ArrayObject
580
    {
581
        $parameter = [
582
            'name' => 'id',
583
            'in' => 'path',
584
            'required' => true,
585
        ];
586
        $v3 ? $parameter['schema'] = ['type' => 'string'] : $parameter['type'] = 'string';
587
        $pathOperation['parameters'] ?? $pathOperation['parameters'] = [$parameter];
588
589
        return $pathOperation;
590
    }
591
592
    private function getJsonSchema(bool $v3, \ArrayObject $definitions, string $resourceClass, string $type, ?string $operationType, ?string $operationName, string $format = 'json', ?array $serializerContext = null, bool $forceCollection = false): Schema
593
    {
594
        $schema = new Schema($v3 ? Schema::VERSION_OPENAPI : Schema::VERSION_SWAGGER);
595
        $schema->setDefinitions($definitions);
596
597
        $this->jsonSchemaFactory->buildSchema($resourceClass, $format, $type, $operationType, $operationName, $schema, $serializerContext, $forceCollection);
598
599
        return $schema;
600
    }
601
602
    private function computeDoc(bool $v3, Documentation $documentation, \ArrayObject $definitions, \ArrayObject $paths, array $context): array
603
    {
604
        $baseUrl = $context[self::BASE_URL] ?? $this->defaultContext[self::BASE_URL];
605
606
        if ($v3) {
607
            $docs = ['openapi' => self::OPENAPI_VERSION];
608
            if ('/' !== $baseUrl && '' !== $baseUrl) {
609
                $docs['servers'] = [['url' => $baseUrl]];
610
            }
611
        } else {
612
            $docs = [
613
                'swagger' => self::SWAGGER_VERSION,
614
                'basePath' => $baseUrl,
615
            ];
616
        }
617
618
        $docs += [
619
            'info' => [
620
                'title' => $documentation->getTitle(),
621
                'version' => $documentation->getVersion(),
622
            ],
623
            'paths' => $paths,
624
        ];
625
626
        if ('' !== $description = $documentation->getDescription()) {
627
            $docs['info']['description'] = $description;
628
        }
629
630
        $securityDefinitions = [];
631
        $security = [];
632
633
        if ($this->oauthEnabled) {
634
            $oauthAttributes = [
635
                'tokenUrl' => $this->oauthTokenUrl,
636
                'authorizationUrl' => $this->oauthAuthorizationUrl,
637
                'scopes' => $this->oauthScopes,
638
            ];
639
640
            $securityDefinitions['oauth'] = [
641
                'type' => $this->oauthType,
642
                'description' => sprintf(
643
                    'OAuth 2.0 %s Grant',
644
                    strtolower(preg_replace('/[A-Z]/', ' \\0', lcfirst($this->oauthFlow)))
645
                ),
646
            ];
647
648
            if ($v3) {
649
                $securityDefinitions['oauth']['flows'] = [
650
                    $this->oauthFlow => $oauthAttributes,
651
                ];
652
            } else {
653
                $securityDefinitions['oauth']['flow'] = $this->oauthFlow;
654
                $securityDefinitions['oauth'] = array_merge($securityDefinitions['oauth'], $oauthAttributes);
655
            }
656
657
            $security[] = ['oauth' => []];
658
        }
659
660
        foreach ($this->apiKeys as $key => $apiKey) {
661
            $name = $apiKey['name'];
662
            $type = $apiKey['type'];
663
664
            $securityDefinitions[$key] = [
665
                'type' => 'apiKey',
666
                'in' => $type,
667
                'description' => sprintf('Value for the %s %s', $name, 'query' === $type ? sprintf('%s parameter', $type) : $type),
668
                'name' => $name,
669
            ];
670
671
            $security[] = [$key => []];
672
        }
673
674
        if ($v3) {
675
            if ($securityDefinitions && $security) {
676
                $docs['security'] = $security;
677
            }
678
        } elseif ($securityDefinitions && $security) {
679
            $docs['securityDefinitions'] = $securityDefinitions;
680
            $docs['security'] = $security;
681
        }
682
683
        if ($v3) {
684
            if (\count($definitions) + \count($securityDefinitions)) {
685
                $docs['components'] = [];
686
                if (\count($definitions)) {
687
                    $docs['components']['schemas'] = $definitions;
688
                }
689
                if (\count($securityDefinitions)) {
690
                    $docs['components']['securitySchemes'] = $securityDefinitions;
691
                }
692
            }
693
        } elseif (\count($definitions) > 0) {
694
            $docs['definitions'] = $definitions;
695
        }
696
697
        return $docs;
698
    }
699
700
    /**
701
     * Gets parameters corresponding to enabled filters.
702
     */
703
    private function getFiltersParameters(bool $v3, string $resourceClass, string $operationName, ResourceMetadata $resourceMetadata): array
704
    {
705
        if (null === $this->filterLocator) {
706
            return [];
707
        }
708
709
        $parameters = [];
710
        $resourceFilters = $resourceMetadata->getCollectionOperationAttribute($operationName, 'filters', [], true);
711
        foreach ($resourceFilters as $filterId) {
712
            if (!$filter = $this->getFilter($filterId)) {
713
                continue;
714
            }
715
716
            foreach ($filter->getDescription($resourceClass) as $name => $data) {
717
                $parameter = [
718
                    'name' => $name,
719
                    'in' => 'query',
720
                    'required' => $data['required'],
721
                ];
722
723
                $type = \in_array($data['type'], Type::$builtinTypes, true) ? $this->jsonSchemaTypeFactory->getType(new Type($data['type'], false, null, $data['is_collection'] ?? false)) : ['type' => 'string'];
724
                $v3 ? $parameter['schema'] = $type : $parameter += $type;
725
726
                if ($v3 && isset($data['schema'])) {
727
                    $parameter['schema'] = $data['schema'];
728
                }
729
730
                if ('array' === ($type['type'] ?? '')) {
731
                    $deepObject = \in_array($data['type'], [Type::BUILTIN_TYPE_ARRAY, Type::BUILTIN_TYPE_OBJECT], true);
732
733
                    if ($v3) {
734
                        $parameter['style'] = $deepObject ? 'deepObject' : 'form';
735
                        $parameter['explode'] = true;
736
                    } else {
737
                        $parameter['collectionFormat'] = $deepObject ? 'csv' : 'multi';
738
                    }
739
                }
740
741
                $key = $v3 ? 'openapi' : 'swagger';
742
                if (isset($data[$key])) {
743
                    $parameter = $data[$key] + $parameter;
744
                }
745
746
                $parameters[] = $parameter;
747
            }
748
        }
749
750
        return $parameters;
751
    }
752
753
    /**
754
     * {@inheritdoc}
755
     */
756
    public function supportsNormalization($data, $format = null): bool
757
    {
758
        return self::FORMAT === $format && $data instanceof Documentation;
759
    }
760
761
    /**
762
     * {@inheritdoc}
763
     */
764
    public function hasCacheableSupportsMethod(): bool
765
    {
766
        return true;
767
    }
768
769
    private function flattenMimeTypes(array $responseFormats): array
770
    {
771
        $responseMimeTypes = [];
772
        foreach ($responseFormats as $responseFormat => $mimeTypes) {
773
            foreach ($mimeTypes as $mimeType) {
774
                $responseMimeTypes[$mimeType] = $responseFormat;
775
            }
776
        }
777
778
        return $responseMimeTypes;
779
    }
780
781
    /**
782
     * https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.0.md#linkObject.
783
     */
784
    private function getLinkObject(string $resourceClass, string $operationId, string $path): array
785
    {
786
        $linkObject = $identifiers = [];
787
        foreach ($this->propertyNameCollectionFactory->create($resourceClass) as $propertyName) {
788
            $propertyMetadata = $this->propertyMetadataFactory->create($resourceClass, $propertyName);
789
            if (!$propertyMetadata->isIdentifier()) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $propertyMetadata->isIdentifier() of type boolean|null is loosely compared to false; this is ambiguous if the boolean can be false. You might want to explicitly use !== null instead.

If an expression can have both false, and null as possible values. It is generally a good practice to always use strict comparison to clearly distinguish between those two values.

$a = canBeFalseAndNull();

// Instead of
if ( ! $a) { }

// Better use one of the explicit versions:
if ($a !== null) { }
if ($a !== false) { }
if ($a !== null && $a !== false) { }
Loading history...
790
                continue;
791
            }
792
793
            $linkObject['parameters'][$propertyName] = sprintf('$response.body#/%s', $propertyName);
794
            $identifiers[] = $propertyName;
795
        }
796
797
        if (!$linkObject) {
798
            return [];
799
        }
800
        $linkObject['operationId'] = $operationId;
801
        $linkObject['description'] = 1 === \count($identifiers) ? sprintf('The `%1$s` value returned in the response can be used as the `%1$s` parameter in `GET %2$s`.', $identifiers[0], $path) : sprintf('The values returned in the response can be used in `GET %s`.', $path);
802
803
        return $linkObject;
804
    }
805
}
806