DocumentationNormalizer::updateGetOperation()   A
last analyzed

Complexity

Conditions 3
Paths 4

Size

Total Lines 35
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 3
eloc 19
c 2
b 0
f 0
nc 4
nop 9
dl 0
loc 35
rs 9.6333

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\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