Passed
Pull Request — master (#2983)
by Kévin
04:30
created

DocumentationNormalizer::updatePostOperation()   A

Complexity

Conditions 6
Paths 8

Size

Total Lines 27
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 6
eloc 15
c 1
b 0
f 0
nc 8
nop 11
dl 0
loc 27
rs 9.2222

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
        self::SPEC_VERSION => 2,
99
        ApiGatewayNormalizer::API_GATEWAY => false,
100
    ];
101
102
    /**
103
     * @param SchemaFactoryInterface|ResourceClassResolverInterface|null $jsonSchemaFactory
104
     * @param ContainerInterface|FilterCollection|null                   $filterLocator
105
     * @param array|OperationAwareFormatsProviderInterface               $formats
106
     * @param mixed|null                                                 $jsonSchemaTypeFactory
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 = [])
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 = array_merge($this->defaultContext, $defaultContext);
167
    }
168
169
    /**
170
     * {@inheritdoc}
171
     */
172
    public function normalize($object, $format = null, array $context = [])
173
    {
174
        $v3 = 3 === ($context['spec_version'] ?? $this->defaultContext['spec_version']) && !($context['api_gateway'] ?? $this->defaultContext['api_gateway']);
175
176
        $definitions = new \ArrayObject();
177
        $paths = new \ArrayObject();
178
        $links = new \ArrayObject();
179
180
        foreach ($object->getResourceNameCollection() as $resourceClass) {
181
            $resourceMetadata = $this->resourceMetadataFactory->create($resourceClass);
182
            $resourceShortName = $resourceMetadata->getShortName();
183
184
            // Items needs to be parsed first to be able to reference the lines from the collection operation
185
            $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

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

253
        if ($v3 && 'GET' === $method && OperationType::ITEM === $operationType && $link = $this->getLinkObject($resourceClass, $pathOperation['operationId'], $this->getPath(/** @scrutinizer ignore-type */ $resourceShortName, $operationName, $operation, $operationType))) {
Loading history...
254
            $links[$pathOperation['operationId']] = $link;
255
        }
256
        if ($resourceMetadata->getTypedOperationAttribute($operationType, $operationName, 'deprecation_reason', null, true)) {
257
            $pathOperation['deprecated'] = true;
258
        }
259
260
        if (null === $this->formatsProvider) {
261
            $requestFormats = $resourceMetadata->getTypedOperationAttribute($operationType, $operationName, 'input_formats', [], true);
262
            $responseFormats = $resourceMetadata->getTypedOperationAttribute($operationType, $operationName, 'output_formats', [], true);
263
        } else {
264
            $requestFormats = $responseFormats = $this->formatsProvider->getFormatsFromOperation($resourceClass, $operationName, $operationType);
265
        }
266
267
        $requestMimeTypes = $this->flattenMimeTypes($requestFormats);
268
        $responseMimeTypes = $this->flattenMimeTypes($responseFormats);
269
        switch ($method) {
270
            case 'GET':
271
                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

271
                return $this->updateGetOperation($v3, $pathOperation, $responseMimeTypes, $operationType, $resourceMetadata, $resourceClass, /** @scrutinizer ignore-type */ $resourceShortName, $operationName, $definitions);
Loading history...
272
            case 'POST':
273
                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

273
                return $this->updatePostOperation($v3, $pathOperation, $requestMimeTypes, $responseMimeTypes, $operationType, $resourceMetadata, $resourceClass, /** @scrutinizer ignore-type */ $resourceShortName, $operationName, $definitions, $links);
Loading history...
274
            case 'PATCH':
275
                $pathOperation['summary'] ?? $pathOperation['summary'] = sprintf('Updates the %s resource.', $resourceShortName);
276
            // no break
277
            case 'PUT':
278
                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

278
                return $this->updatePutOperation($v3, $pathOperation, $requestMimeTypes, $responseMimeTypes, $operationType, $resourceMetadata, $resourceClass, /** @scrutinizer ignore-type */ $resourceShortName, $operationName, $definitions);
Loading history...
279
            case 'DELETE':
280
                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

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