Passed
Push — master ( 5dfc18...f5f77d )
by Kévin
06:44 queued 03:20
created

DocumentationNormalizer::getPathOperation()   C

Complexity

Conditions 13
Paths 64

Size

Total Lines 31
Code Lines 23

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 23
dl 0
loc 31
rs 6.6166
c 0
b 0
f 0
cc 13
nc 64
nop 10

How to fix   Complexity    Many Parameters   

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:

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\OperationAwareFormatsProviderInterface;
19
use ApiPlatform\Core\Api\OperationMethodResolverInterface;
20
use ApiPlatform\Core\Api\OperationType;
21
use ApiPlatform\Core\Api\ResourceClassResolverInterface;
22
use ApiPlatform\Core\Api\UrlGeneratorInterface;
23
use ApiPlatform\Core\Documentation\Documentation;
24
use ApiPlatform\Core\Metadata\Property\Factory\PropertyMetadataFactoryInterface;
25
use ApiPlatform\Core\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface;
26
use ApiPlatform\Core\Metadata\Property\PropertyMetadata;
27
use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface;
28
use ApiPlatform\Core\Metadata\Resource\ResourceMetadata;
29
use ApiPlatform\Core\Operation\Factory\SubresourceOperationFactoryInterface;
30
use ApiPlatform\Core\PathResolver\OperationPathResolverInterface;
31
use Psr\Container\ContainerInterface;
32
use Symfony\Component\PropertyInfo\Type;
33
use Symfony\Component\Serializer\NameConverter\NameConverterInterface;
34
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
35
use Symfony\Component\Serializer\Normalizer\CacheableSupportsMethodInterface;
36
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
37
38
/**
39
 * Generates an OpenAPI specification (formerly known as Swagger). OpenAPI v2 and v3 are supported.
40
 *
41
 * @author Amrouche Hamza <[email protected]>
42
 * @author Teoh Han Hui <[email protected]>
43
 * @author Kévin Dunglas <[email protected]>
44
 * @author Anthony GRASSIOT <[email protected]>
45
 */
46
final class DocumentationNormalizer implements NormalizerInterface, CacheableSupportsMethodInterface
47
{
48
    use FilterLocatorTrait;
49
50
    const FORMAT = 'json';
51
    const BASE_URL = 'base_url';
52
    const SPEC_VERSION = 'spec_version';
53
    const OPENAPI_VERSION = '3.0.2';
54
    const SWAGGER_DEFINITION_NAME = 'swagger_definition_name';
55
    const SWAGGER_VERSION = '2.0';
56
57
    /**
58
     * @deprecated
59
     */
60
    const ATTRIBUTE_NAME = 'swagger_context';
61
62
    private $resourceMetadataFactory;
63
    private $propertyNameCollectionFactory;
64
    private $propertyMetadataFactory;
65
    private $resourceClassResolver;
66
    private $operationMethodResolver;
67
    private $operationPathResolver;
68
    private $nameConverter;
69
    private $oauthEnabled;
70
    private $oauthType;
71
    private $oauthFlow;
72
    private $oauthTokenUrl;
73
    private $oauthAuthorizationUrl;
74
    private $oauthScopes;
75
    private $apiKeys;
76
    private $subresourceOperationFactory;
77
    private $paginationEnabled;
78
    private $paginationPageParameterName;
79
    private $clientItemsPerPage;
80
    private $itemsPerPageParameterName;
81
    private $paginationClientEnabled;
82
    private $paginationClientEnabledParameterName;
83
    private $formatsProvider;
84
    private $defaultContext = [
85
        self::BASE_URL => '/',
86
        self::SPEC_VERSION => 2,
87
        ApiGatewayNormalizer::API_GATEWAY => false,
88
    ];
89
90
    /**
91
     * @param ContainerInterface|FilterCollection|null $filterLocator The new filter locator or the deprecated filter collection
92
     */
93
    public function __construct(ResourceMetadataFactoryInterface $resourceMetadataFactory, PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, ResourceClassResolverInterface $resourceClassResolver, OperationMethodResolverInterface $operationMethodResolver, OperationPathResolverInterface $operationPathResolver, UrlGeneratorInterface $urlGenerator = null, $filterLocator = null, NameConverterInterface $nameConverter = null, bool $oauthEnabled = false, string $oauthType = '', string $oauthFlow = '', string $oauthTokenUrl = '', string $oauthAuthorizationUrl = '', array $oauthScopes = [], array $apiKeys = [], SubresourceOperationFactoryInterface $subresourceOperationFactory = null, bool $paginationEnabled = true, string $paginationPageParameterName = 'page', bool $clientItemsPerPage = false, string $itemsPerPageParameterName = 'itemsPerPage', OperationAwareFormatsProviderInterface $formatsProvider = null, bool $paginationClientEnabled = false, string $paginationClientEnabledParameterName = 'pagination', array $defaultContext = [])
94
    {
95
        if ($urlGenerator) {
96
            @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);
97
        }
98
99
        $this->setFilterLocator($filterLocator, true);
100
101
        $this->resourceMetadataFactory = $resourceMetadataFactory;
102
        $this->propertyNameCollectionFactory = $propertyNameCollectionFactory;
103
        $this->propertyMetadataFactory = $propertyMetadataFactory;
104
        $this->resourceClassResolver = $resourceClassResolver;
105
        $this->operationMethodResolver = $operationMethodResolver;
106
        $this->operationPathResolver = $operationPathResolver;
107
        $this->nameConverter = $nameConverter;
108
        $this->oauthEnabled = $oauthEnabled;
109
        $this->oauthType = $oauthType;
110
        $this->oauthFlow = $oauthFlow;
111
        $this->oauthTokenUrl = $oauthTokenUrl;
112
        $this->oauthAuthorizationUrl = $oauthAuthorizationUrl;
113
        $this->oauthScopes = $oauthScopes;
114
        $this->subresourceOperationFactory = $subresourceOperationFactory;
115
        $this->paginationEnabled = $paginationEnabled;
116
        $this->paginationPageParameterName = $paginationPageParameterName;
117
        $this->apiKeys = $apiKeys;
118
        $this->subresourceOperationFactory = $subresourceOperationFactory;
119
        $this->clientItemsPerPage = $clientItemsPerPage;
120
        $this->itemsPerPageParameterName = $itemsPerPageParameterName;
121
        $this->formatsProvider = $formatsProvider;
122
        $this->paginationClientEnabled = $paginationClientEnabled;
123
        $this->paginationClientEnabledParameterName = $paginationClientEnabledParameterName;
124
125
        $this->defaultContext = array_merge($this->defaultContext, $defaultContext);
126
    }
127
128
    /**
129
     * {@inheritdoc}
130
     */
131
    public function normalize($object, $format = null, array $context = [])
132
    {
133
        $v3 = 3 === ($context['spec_version'] ?? $this->defaultContext['spec_version']) && !($context['api_gateway'] ?? $this->defaultContext['api_gateway']);
134
135
        $mimeTypes = $object->getMimeTypes();
136
        $definitions = new \ArrayObject();
137
        $paths = new \ArrayObject();
138
        $links = new \ArrayObject();
139
140
        foreach ($object->getResourceNameCollection() as $resourceClass) {
141
            $resourceMetadata = $this->resourceMetadataFactory->create($resourceClass);
142
            $resourceShortName = $resourceMetadata->getShortName();
143
144
            // Items needs to be parsed first to be able to reference the lines from the collection operation
145
            $this->addPaths($v3, $paths, $definitions, $resourceClass, $resourceShortName, $resourceMetadata, $mimeTypes, 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

145
            $this->addPaths($v3, $paths, $definitions, $resourceClass, /** @scrutinizer ignore-type */ $resourceShortName, $resourceMetadata, $mimeTypes, OperationType::ITEM, $links);
Loading history...
146
            $this->addPaths($v3, $paths, $definitions, $resourceClass, $resourceShortName, $resourceMetadata, $mimeTypes, OperationType::COLLECTION, $links);
147
148
            if (null === $this->subresourceOperationFactory) {
149
                continue;
150
            }
151
152
            foreach ($this->subresourceOperationFactory->create($resourceClass) as $operationId => $subresourceOperation) {
153
                $operationName = 'get';
154
                $subResourceMetadata = $this->resourceMetadataFactory->create($subresourceOperation['resource_class']);
155
                $serializerContext = $this->getSerializerContext(OperationType::SUBRESOURCE, false, $subResourceMetadata, $operationName);
156
157
                $responseDefinitionKey = false;
158
                if (false !== $outputClass = $subResourceMetadata->getSubresourceOperationAttribute($operationName, 'output_class')) {
159
                    $responseDefinitionKey = $this->getDefinition($v3, $definitions, $subResourceMetadata, $subresourceOperation['resource_class'], $outputClass, $serializerContext);
160
                }
161
162
                $pathOperation = new \ArrayObject([]);
163
                $pathOperation['tags'] = $subresourceOperation['shortNames'];
164
                $pathOperation['operationId'] = $operationId;
165
166
                if (null !== $this->formatsProvider) {
167
                    $responseFormats = $this->formatsProvider->getFormatsFromOperation($subresourceOperation['resource_class'], $operationName, OperationType::SUBRESOURCE);
168
                    $responseMimeTypes = $this->extractMimeTypes($responseFormats);
169
                }
170
                if (!$v3) {
171
                    $pathOperation['produces'] = $responseMimeTypes ?? $mimeTypes;
172
                }
173
174
                if ($subresourceOperation['collection']) {
175
                    $baseSuccessResponse = ['description' => sprintf('%s collection response', $subresourceOperation['shortNames'][0])];
176
177
                    if ($responseDefinitionKey) {
178
                        if ($v3) {
179
                            $baseSuccessResponse['content'] = array_fill_keys($responseMimeTypes ?? $mimeTypes, ['schema' => ['type' => 'array', 'items' => ['$ref' => sprintf('#/components/schemas/%s', $responseDefinitionKey)]]]);
180
                        } else {
181
                            $baseSuccessResponse['schema'] = ['type' => 'array', 'items' => ['$ref' => sprintf('#/definitions/%s', $responseDefinitionKey)]];
182
                        }
183
                    }
184
                } else {
185
                    $baseSuccessResponse = ['description' => sprintf('%s resource response', $subresourceOperation['shortNames'][0])];
186
187
                    if ($responseDefinitionKey) {
188
                        if ($v3) {
189
                            $baseSuccessResponse['content'] = array_fill_keys($responseMimeTypes ?? $mimeTypes, ['schema' => ['$ref' => sprintf('#/components/schemas/%s', $responseDefinitionKey)]]);
190
                        } else {
191
                            $baseSuccessResponse['schema'] = ['$ref' => sprintf('#/definitions/%s', $responseDefinitionKey)];
192
                        }
193
                    }
194
                }
195
196
                $pathOperation['summary'] = sprintf('Retrieves %s%s resource%s.', $subresourceOperation['collection'] ? 'the collection of ' : 'a ', $subresourceOperation['shortNames'][0], $subresourceOperation['collection'] ? 's' : '');
197
                $pathOperation['responses'] = [
198
                    (string) $resourceMetadata->getSubresourceOperationAttribute($operationName, 'status', '200') => $baseSuccessResponse,
199
                    '404' => ['description' => 'Resource not found'],
200
                ];
201
202
                // Avoid duplicates parameters when there is a filter on a subresource identifier
203
                $parametersMemory = [];
204
                $pathOperation['parameters'] = [];
205
206
                foreach ($subresourceOperation['identifiers'] as list($identifier, , $hasIdentifier)) {
207
                    if (true === $hasIdentifier) {
208
                        $parameter = [
209
                            'name' => $identifier,
210
                            'in' => 'path',
211
                            'required' => true,
212
                        ];
213
                        $v3 ? $parameter['schema'] = ['type' => 'string'] : $parameter['type'] = 'string';
214
215
                        $pathOperation['parameters'][] = $parameter;
216
                        $parametersMemory[] = $identifier;
217
                    }
218
                }
219
220
                if ($parameters = $this->getFiltersParameters($v3, $subresourceOperation['resource_class'], $operationName, $subResourceMetadata, $definitions, $serializerContext)) {
221
                    foreach ($parameters as $parameter) {
222
                        if (!\in_array($parameter['name'], $parametersMemory, true)) {
223
                            $pathOperation['parameters'][] = $parameter;
224
                        }
225
                    }
226
                }
227
228
                $paths[$this->getPath($subresourceOperation['shortNames'][0], $subresourceOperation['route_name'], $subresourceOperation, OperationType::SUBRESOURCE)] = new \ArrayObject(['get' => $pathOperation]);
229
            }
230
        }
231
232
        $definitions->ksort();
233
        $paths->ksort();
234
235
        return $this->computeDoc($v3, $object, $definitions, $paths, $context);
236
    }
237
238
    /**
239
     * Updates the list of entries in the paths collection.
240
     */
241
    private function addPaths(bool $v3, \ArrayObject $paths, \ArrayObject $definitions, string $resourceClass, string $resourceShortName, ResourceMetadata $resourceMetadata, array $mimeTypes, string $operationType, \ArrayObject $links)
242
    {
243
        if (null === $operations = OperationType::COLLECTION === $operationType ? $resourceMetadata->getCollectionOperations() : $resourceMetadata->getItemOperations()) {
244
            return;
245
        }
246
247
        foreach ($operations as $operationName => $operation) {
248
            $path = $this->getPath($resourceShortName, $operationName, $operation, $operationType);
249
            $method = OperationType::ITEM === $operationType ? $this->operationMethodResolver->getItemOperationMethod($resourceClass, $operationName) : $this->operationMethodResolver->getCollectionOperationMethod($resourceClass, $operationName);
250
251
            $paths[$path][strtolower($method)] = $this->getPathOperation($v3, $operationName, $operation, $method, $operationType, $resourceClass, $resourceMetadata, $mimeTypes, $definitions, $links);
252
        }
253
    }
254
255
    /**
256
     * Gets the path for an operation.
257
     *
258
     * If the path ends with the optional _format parameter, it is removed
259
     * as optional path parameters are not yet supported.
260
     *
261
     * @see https://github.com/OAI/OpenAPI-Specification/issues/93
262
     */
263
    private function getPath(string $resourceShortName, string $operationName, array $operation, string $operationType): string
264
    {
265
        $path = $this->operationPathResolver->resolveOperationPath($resourceShortName, $operation, $operationType, $operationName);
266
        if ('.{_format}' === substr($path, -10)) {
267
            $path = substr($path, 0, -10);
268
        }
269
270
        return $path;
271
    }
272
273
    /**
274
     * Gets a path Operation Object.
275
     *
276
     * @see https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#operation-object
277
     *
278
     * @param string[] $mimeTypes
279
     */
280
    private function getPathOperation(bool $v3, string $operationName, array $operation, string $method, string $operationType, string $resourceClass, ResourceMetadata $resourceMetadata, array $mimeTypes, \ArrayObject $definitions, \ArrayObject $links): \ArrayObject
281
    {
282
        $pathOperation = new \ArrayObject($operation[$v3 ? 'openapi_context' : 'swagger_context'] ?? []);
283
        $resourceShortName = $resourceMetadata->getShortName();
284
        $pathOperation['tags'] ?? $pathOperation['tags'] = [$resourceShortName];
285
        $pathOperation['operationId'] ?? $pathOperation['operationId'] = lcfirst($operationName).ucfirst($resourceShortName).ucfirst($operationType);
286
        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

286
        if ($v3 && 'GET' === $method && OperationType::ITEM === $operationType && $link = $this->getLinkObject($resourceClass, $pathOperation['operationId'], $this->getPath(/** @scrutinizer ignore-type */ $resourceShortName, $operationName, $operation, $operationType))) {
Loading history...
287
            $links[$pathOperation['operationId']] = $link;
288
        }
289
        if ($resourceMetadata->getTypedOperationAttribute($operationType, $operationName, 'deprecation_reason', null, true)) {
290
            $pathOperation['deprecated'] = true;
291
        }
292
        if (null !== $this->formatsProvider) {
293
            $responseFormats = $this->formatsProvider->getFormatsFromOperation($resourceClass, $operationName, $operationType);
294
            $responseMimeTypes = $this->extractMimeTypes($responseFormats);
295
        }
296
        switch ($method) {
297
            case 'GET':
298
                return $this->updateGetOperation($v3, $pathOperation, $responseMimeTypes ?? $mimeTypes, $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

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

300
                return $this->updatePostOperation($v3, $pathOperation, $responseMimeTypes ?? $mimeTypes, $operationType, $resourceMetadata, $resourceClass, /** @scrutinizer ignore-type */ $resourceShortName, $operationName, $definitions, $links);
Loading history...
301
            case 'PATCH':
302
                $pathOperation['summary'] ?? $pathOperation['summary'] = sprintf('Updates the %s resource.', $resourceShortName);
303
            // no break
304
            case 'PUT':
305
                return $this->updatePutOperation($v3, $pathOperation, $responseMimeTypes ?? $mimeTypes, $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

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

307
                return $this->updateDeleteOperation($v3, $pathOperation, /** @scrutinizer ignore-type */ $resourceShortName, $operationType, $operationName, $resourceMetadata);
Loading history...
308
        }
309
310
        return $pathOperation;
311
    }
312
313
    private function updateGetOperation(bool $v3, \ArrayObject $pathOperation, array $mimeTypes, string $operationType, ResourceMetadata $resourceMetadata, string $resourceClass, string $resourceShortName, string $operationName, \ArrayObject $definitions): \ArrayObject
314
    {
315
        $serializerContext = $this->getSerializerContext($operationType, false, $resourceMetadata, $operationName);
316
317
        $responseDefinitionKey = false;
318
        if (false !== $outputClass = $resourceMetadata->getTypedOperationAttribute($operationType, $operationName, 'output_class', null, true)) {
319
            $responseDefinitionKey = $this->getDefinition($v3, $definitions, $resourceMetadata, $resourceClass, $outputClass, $serializerContext);
320
        }
321
322
        $successStatus = (string) $resourceMetadata->getTypedOperationAttribute($operationType, $operationName, 'status', '200');
323
324
        if (!$v3) {
325
            $pathOperation['produces'] ?? $pathOperation['produces'] = $mimeTypes;
326
        }
327
328
        if (OperationType::COLLECTION === $operationType) {
329
            $pathOperation['summary'] ?? $pathOperation['summary'] = sprintf('Retrieves the collection of %s resources.', $resourceShortName);
330
331
            $successResponse = ['description' => sprintf('%s collection response', $resourceShortName)];
332
333
            if ($responseDefinitionKey) {
334
                if ($v3) {
335
                    $successResponse['content'] = array_fill_keys($mimeTypes, [
336
                        'schema' => [
337
                            'type' => 'array',
338
                            'items' => ['$ref' => sprintf('#/components/schemas/%s', $responseDefinitionKey)],
339
                        ],
340
                    ]);
341
                } else {
342
                    $successResponse['schema'] = [
343
                        'type' => 'array',
344
                        'items' => ['$ref' => sprintf('#/definitions/%s', $responseDefinitionKey)],
345
                    ];
346
                }
347
            }
348
349
            $pathOperation['responses'] ?? $pathOperation['responses'] = [$successStatus => $successResponse];
350
            $pathOperation['parameters'] ?? $pathOperation['parameters'] = $this->getFiltersParameters($v3, $resourceClass, $operationName, $resourceMetadata, $definitions, $serializerContext);
351
352
            if ($this->paginationEnabled && $resourceMetadata->getCollectionOperationAttribute($operationName, 'pagination_enabled', true, true)) {
353
                $paginationParameter = [
354
                    'name' => $this->paginationPageParameterName,
355
                    'in' => 'query',
356
                    'required' => false,
357
                    'description' => 'The collection page number',
358
                ];
359
                $v3 ? $paginationParameter['schema'] = ['type' => 'integer'] : $paginationParameter['type'] = 'integer';
360
                $pathOperation['parameters'][] = $paginationParameter;
361
362
                if ($resourceMetadata->getCollectionOperationAttribute($operationName, 'pagination_client_items_per_page', $this->clientItemsPerPage, true)) {
363
                    $itemPerPageParameter = [
364
                        'name' => $this->itemsPerPageParameterName,
365
                        'in' => 'query',
366
                        'required' => false,
367
                        'description' => 'The number of items per page',
368
                    ];
369
                    $v3 ? $itemPerPageParameter['schema'] = ['type' => 'integer'] : $itemPerPageParameter['type'] = 'integer';
370
371
                    $pathOperation['parameters'][] = $itemPerPageParameter;
372
                }
373
            }
374
375
            if ($this->paginationEnabled && $resourceMetadata->getCollectionOperationAttribute($operationName, 'pagination_client_enabled', $this->paginationClientEnabled, true)) {
376
                $paginationEnabledParameter = [
377
                    'name' => $this->paginationClientEnabledParameterName,
378
                    'in' => 'query',
379
                    'required' => false,
380
                    'description' => 'Enable or disable pagination',
381
                ];
382
                $v3 ? $paginationEnabledParameter['schema'] = ['type' => 'boolean'] : $paginationEnabledParameter['type'] = 'boolean';
383
                $pathOperation['parameters'][] = $paginationEnabledParameter;
384
            }
385
386
            return $pathOperation;
387
        }
388
389
        $pathOperation['summary'] ?? $pathOperation['summary'] = sprintf('Retrieves a %s resource.', $resourceShortName);
390
391
        $parameter = [
392
            'name' => 'id',
393
            'in' => 'path',
394
            'required' => true,
395
        ];
396
        $v3 ? $parameter['schema'] = ['type' => 'string'] : $parameter['type'] = 'string';
397
        $pathOperation['parameters'] ?? $pathOperation['parameters'] = [$parameter];
398
399
        $successResponse = ['description' => sprintf('%s resource response', $resourceShortName)];
400
        if ($responseDefinitionKey) {
401
            if ($v3) {
402
                $successResponse['content'] = array_fill_keys($mimeTypes, ['schema' => ['$ref' => sprintf('#/components/schemas/%s', $responseDefinitionKey)]]);
403
            } else {
404
                $successResponse['schema'] = ['$ref' => sprintf('#/definitions/%s', $responseDefinitionKey)];
405
            }
406
        }
407
408
        $pathOperation['responses'] ?? $pathOperation['responses'] = [
409
            $successStatus => $successResponse,
410
            '404' => ['description' => 'Resource not found'],
411
        ];
412
413
        return $pathOperation;
414
    }
415
416
    private function updatePostOperation(bool $v3, \ArrayObject $pathOperation, array $mimeTypes, string $operationType, ResourceMetadata $resourceMetadata, string $resourceClass, string $resourceShortName, string $operationName, \ArrayObject $definitions, \ArrayObject $links): \ArrayObject
417
    {
418
        if (!$v3) {
419
            $pathOperation['consumes'] ?? $pathOperation['consumes'] = $mimeTypes;
420
            $pathOperation['produces'] ?? $pathOperation['produces'] = $mimeTypes;
421
        }
422
423
        $pathOperation['summary'] ?? $pathOperation['summary'] = sprintf('Creates a %s resource.', $resourceShortName);
424
425
        $responseDefinitionKey = false;
426
        if (false !== $outputClass = $resourceMetadata->getTypedOperationAttribute($operationType, $operationName, 'output_class', null, true)) {
427
            $responseDefinitionKey = $this->getDefinition($v3, $definitions, $resourceMetadata, $resourceClass, $outputClass, $this->getSerializerContext($operationType, false, $resourceMetadata, $operationName));
428
        }
429
430
        $successResponse = ['description' => sprintf('%s resource created', $resourceShortName)];
431
        if ($responseDefinitionKey) {
432
            if ($v3) {
433
                $successResponse['content'] = array_fill_keys($mimeTypes, ['schema' => ['$ref' => sprintf('#/components/schemas/%s', $responseDefinitionKey)]]);
434
                if ($links[$key = 'get'.ucfirst($resourceShortName).ucfirst(OperationType::ITEM)] ?? null) {
435
                    $successResponse['links'] = [ucfirst($key) => $links[$key]];
436
                }
437
            } else {
438
                $successResponse['schema'] = ['$ref' => sprintf('#/definitions/%s', $responseDefinitionKey)];
439
            }
440
        }
441
442
        $pathOperation['responses'] ?? $pathOperation['responses'] = [
443
            (string) $resourceMetadata->getTypedOperationAttribute($operationType, $operationName, 'status', '201') => $successResponse,
444
            '400' => ['description' => 'Invalid input'],
445
            '404' => ['description' => 'Resource not found'],
446
        ];
447
448
        if (false === $inputClass = $resourceMetadata->getTypedOperationAttribute($operationType, $operationName, 'input_class', null, true)) {
449
            return $pathOperation;
450
        }
451
452
        $requestDefinitionKey = $this->getDefinition($v3, $definitions, $resourceMetadata, $resourceClass, $inputClass, $this->getSerializerContext($operationType, true, $resourceMetadata, $operationName));
453
        if ($v3) {
454
            $pathOperation['requestBody'] ?? $pathOperation['requestBody'] = [
455
                'content' => array_fill_keys($mimeTypes, ['schema' => ['$ref' => sprintf('#/components/schemas/%s', $requestDefinitionKey)]]),
456
                'description' => sprintf('The new %s resource', $resourceShortName),
457
            ];
458
        } else {
459
            $pathOperation['parameters'] ?? $pathOperation['parameters'] = [[
460
                'name' => lcfirst($resourceShortName),
461
                'in' => 'body',
462
                'description' => sprintf('The new %s resource', $resourceShortName),
463
                'schema' => ['$ref' => sprintf('#/definitions/%s', $requestDefinitionKey)],
464
            ]];
465
        }
466
467
        return $pathOperation;
468
    }
469
470
    private function updatePutOperation(bool $v3, \ArrayObject $pathOperation, array $mimeTypes, string $operationType, ResourceMetadata $resourceMetadata, string $resourceClass, string $resourceShortName, string $operationName, \ArrayObject $definitions): \ArrayObject
471
    {
472
        if (!$v3) {
473
            $pathOperation['consumes'] ?? $pathOperation['consumes'] = $mimeTypes;
474
            $pathOperation['produces'] ?? $pathOperation['produces'] = $mimeTypes;
475
        }
476
477
        $pathOperation['summary'] ?? $pathOperation['summary'] = sprintf('Replaces the %s resource.', $resourceShortName);
478
479
        $parameter = [
480
            'name' => 'id',
481
            'in' => 'path',
482
            'required' => true,
483
        ];
484
        $v3 ? $parameter['schema'] = ['type' => 'string'] : $parameter['type'] = 'string';
485
        $pathOperation['parameters'] ?? $pathOperation['parameters'] = [$parameter];
486
487
        $responseDefinitionKey = false;
488
        if (false !== $outputClass = $resourceMetadata->getTypedOperationAttribute($operationType, $operationName, 'output_class', null, true)) {
489
            $responseDefinitionKey = $this->getDefinition($v3, $definitions, $resourceMetadata, $resourceClass, $outputClass, $this->getSerializerContext($operationType, false, $resourceMetadata, $operationName));
490
        }
491
492
        $successResponse = ['description' => sprintf('%s resource updated', $resourceShortName)];
493
        if ($responseDefinitionKey) {
494
            if ($v3) {
495
                $successResponse['content'] = array_fill_keys($mimeTypes, ['schema' => ['$ref' => sprintf('#/components/schemas/%s', $responseDefinitionKey)]]);
496
            } else {
497
                $successResponse['schema'] = ['$ref' => sprintf('#/definitions/%s', $responseDefinitionKey)];
498
            }
499
        }
500
501
        $pathOperation['responses'] ?? $pathOperation['responses'] = [
502
            (string) $resourceMetadata->getTypedOperationAttribute($operationType, $operationName, 'status', '200') => $successResponse,
503
            '400' => ['description' => 'Invalid input'],
504
            '404' => ['description' => 'Resource not found'],
505
        ];
506
507
        if (false === $inputClass = $resourceMetadata->getTypedOperationAttribute($operationType, $operationName, 'input_class', null, true)) {
508
            return $pathOperation;
509
        }
510
511
        $requestDefinitionKey = $this->getDefinition($v3, $definitions, $resourceMetadata, $resourceClass, $inputClass, $this->getSerializerContext($operationType, true, $resourceMetadata, $operationName));
512
        if ($v3) {
513
            $pathOperation['requestBody'] ?? $pathOperation['requestBody'] = [
514
                'content' => array_fill_keys($mimeTypes, ['schema' => ['$ref' => sprintf('#/components/schemas/%s', $requestDefinitionKey)]]),
515
                'description' => sprintf('The updated %s resource', $resourceShortName),
516
            ];
517
        } else {
518
            $pathOperation['parameters'][] = [
519
                'name' => lcfirst($resourceShortName),
520
                'in' => 'body',
521
                'description' => sprintf('The updated %s resource', $resourceShortName),
522
                'schema' => ['$ref' => sprintf('#/definitions/%s', $requestDefinitionKey)],
523
            ];
524
        }
525
526
        return $pathOperation;
527
    }
528
529
    private function updateDeleteOperation(bool $v3, \ArrayObject $pathOperation, string $resourceShortName, string $operationType, string $operationName, ResourceMetadata $resourceMetadata): \ArrayObject
530
    {
531
        $pathOperation['summary'] ?? $pathOperation['summary'] = sprintf('Removes the %s resource.', $resourceShortName);
532
        $pathOperation['responses'] ?? $pathOperation['responses'] = [
533
            (string) $resourceMetadata->getTypedOperationAttribute($operationType, $operationName, 'status', '204') => ['description' => sprintf('%s resource deleted', $resourceShortName)],
534
            '404' => ['description' => 'Resource not found'],
535
        ];
536
537
        $parameter = [
538
            'name' => 'id',
539
            'in' => 'path',
540
            'required' => true,
541
        ];
542
        $v3 ? $parameter['schema'] = ['type' => 'string'] : $parameter['type'] = 'string';
543
544
        $pathOperation['parameters'] ?? $pathOperation['parameters'] = [$parameter];
545
546
        return $pathOperation;
547
    }
548
549
    private function getDefinition(bool $v3, \ArrayObject $definitions, ResourceMetadata $resourceMetadata, string $resourceClass, ?string $publicClass, array $serializerContext = null): string
550
    {
551
        $keyPrefix = $resourceMetadata->getShortName();
552
        if (null !== $publicClass) {
553
            $keyPrefix .= ':'.md5($publicClass);
554
        }
555
556
        if (isset($serializerContext[self::SWAGGER_DEFINITION_NAME])) {
557
            $definitionKey = sprintf('%s-%s', $keyPrefix, $serializerContext[self::SWAGGER_DEFINITION_NAME]);
558
        } else {
559
            $definitionKey = $this->getDefinitionKey($keyPrefix, (array) ($serializerContext[AbstractNormalizer::GROUPS] ?? []));
0 ignored issues
show
Bug introduced by
It seems like $keyPrefix can also be of type null; however, parameter $resourceShortName of ApiPlatform\Core\Swagger...zer::getDefinitionKey() 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

559
            $definitionKey = $this->getDefinitionKey(/** @scrutinizer ignore-type */ $keyPrefix, (array) ($serializerContext[AbstractNormalizer::GROUPS] ?? []));
Loading history...
560
        }
561
562
        if (!isset($definitions[$definitionKey])) {
563
            $definitions[$definitionKey] = [];  // Initialize first to prevent infinite loop
564
            $definitions[$definitionKey] = $this->getDefinitionSchema($v3, $publicClass ?? $resourceClass, $resourceMetadata, $definitions, $serializerContext);
565
        }
566
567
        return $definitionKey;
568
    }
569
570
    private function getDefinitionKey(string $resourceShortName, array $groups): string
571
    {
572
        return $groups ? sprintf('%s-%s', $resourceShortName, implode('_', $groups)) : $resourceShortName;
573
    }
574
575
    /**
576
     * Gets a definition Schema Object.
577
     *
578
     * @see https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#schemaObject
579
     */
580
    private function getDefinitionSchema(bool $v3, string $resourceClass, ResourceMetadata $resourceMetadata, \ArrayObject $definitions, array $serializerContext = null): \ArrayObject
581
    {
582
        $definitionSchema = new \ArrayObject(['type' => 'object']);
583
584
        if (null !== $description = $resourceMetadata->getDescription()) {
585
            $definitionSchema['description'] = $description;
586
        }
587
588
        if (null !== $iri = $resourceMetadata->getIri()) {
589
            $definitionSchema['externalDocs'] = ['url' => $iri];
590
        }
591
592
        $options = isset($serializerContext[AbstractNormalizer::GROUPS]) ? ['serializer_groups' => $serializerContext[AbstractNormalizer::GROUPS]] : [];
593
        foreach ($this->propertyNameCollectionFactory->create($resourceClass, $options) as $propertyName) {
594
            $propertyMetadata = $this->propertyMetadataFactory->create($resourceClass, $propertyName);
595
            $normalizedPropertyName = $this->nameConverter ? $this->nameConverter->normalize($propertyName, $resourceClass, self::FORMAT, $serializerContext ?? []) : $propertyName;
0 ignored issues
show
Unused Code introduced by
The call to Symfony\Component\Serial...rInterface::normalize() has too many arguments starting with $resourceClass. ( Ignorable by Annotation )

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

595
            $normalizedPropertyName = $this->nameConverter ? $this->nameConverter->/** @scrutinizer ignore-call */ normalize($propertyName, $resourceClass, self::FORMAT, $serializerContext ?? []) : $propertyName;

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
596
            if ($propertyMetadata->isRequired()) {
597
                $definitionSchema['required'][] = $normalizedPropertyName;
598
            }
599
600
            $definitionSchema['properties'][$normalizedPropertyName] = $this->getPropertySchema($v3, $propertyMetadata, $definitions, $serializerContext);
601
        }
602
603
        return $definitionSchema;
604
    }
605
606
    /**
607
     * Gets a property Schema Object.
608
     *
609
     * @see https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#schemaObject
610
     */
611
    private function getPropertySchema(bool $v3, PropertyMetadata $propertyMetadata, \ArrayObject $definitions, array $serializerContext = null): \ArrayObject
612
    {
613
        $propertySchema = new \ArrayObject($propertyMetadata->getAttributes()[$v3 ? 'openapi_context' : 'swagger_context'] ?? []);
614
615
        if (false === $propertyMetadata->isWritable() && !$propertyMetadata->isInitializable()) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $propertyMetadata->isInitializable() 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...
616
            $propertySchema['readOnly'] = true;
617
        }
618
619
        if (null !== $description = $propertyMetadata->getDescription()) {
620
            $propertySchema['description'] = $description;
621
        }
622
623
        if (null === $type = $propertyMetadata->getType()) {
624
            return $propertySchema;
625
        }
626
627
        $isCollection = $type->isCollection();
628
        if (null === $valueType = $isCollection ? $type->getCollectionValueType() : $type) {
629
            $builtinType = 'string';
630
            $className = null;
631
        } else {
632
            $builtinType = $valueType->getBuiltinType();
633
            $className = $valueType->getClassName();
634
        }
635
636
        $valueSchema = $this->getType($v3, $builtinType, $isCollection, $className, $propertyMetadata->isReadableLink(), $definitions, $serializerContext);
637
638
        return new \ArrayObject((array) $propertySchema + $valueSchema);
639
    }
640
641
    /**
642
     * Gets the Swagger's type corresponding to the given PHP's type.
643
     */
644
    private function getType(bool $v3, string $type, bool $isCollection, string $className = null, bool $readableLink = null, \ArrayObject $definitions, array $serializerContext = null): array
645
    {
646
        if ($isCollection) {
647
            return ['type' => 'array', 'items' => $this->getType($v3, $type, false, $className, $readableLink, $definitions, $serializerContext)];
648
        }
649
650
        if (Type::BUILTIN_TYPE_STRING === $type) {
651
            return ['type' => 'string'];
652
        }
653
654
        if (Type::BUILTIN_TYPE_INT === $type) {
655
            return ['type' => 'integer'];
656
        }
657
658
        if (Type::BUILTIN_TYPE_FLOAT === $type) {
659
            return ['type' => 'number'];
660
        }
661
662
        if (Type::BUILTIN_TYPE_BOOL === $type) {
663
            return ['type' => 'boolean'];
664
        }
665
666
        if (Type::BUILTIN_TYPE_OBJECT === $type) {
667
            if (null === $className) {
668
                return ['type' => 'string'];
669
            }
670
671
            if (is_subclass_of($className, \DateTimeInterface::class)) {
672
                return ['type' => 'string', 'format' => 'date-time'];
673
            }
674
675
            if (!$this->resourceClassResolver->isResourceClass($className)) {
676
                return ['type' => 'string'];
677
            }
678
679
            if (true === $readableLink) {
680
                return [
681
                    '$ref' => sprintf(
682
                        $v3 ? '#/components/schemas/%s' : '#/definitions/%s',
683
                        $this->getDefinition($v3, $definitions, $resourceMetadata = $this->resourceMetadataFactory->create($className), $className, $resourceMetadata->getAttribute('output_class'), $serializerContext)
684
                    ),
685
                ];
686
            }
687
        }
688
689
        return ['type' => 'string'];
690
    }
691
692
    private function computeDoc(bool $v3, Documentation $documentation, \ArrayObject $definitions, \ArrayObject $paths, array $context): array
693
    {
694
        $baseUrl = $context[self::BASE_URL] ?? $this->defaultContext[self::BASE_URL];
695
696
        if ($v3) {
697
            $docs = ['openapi' => self::OPENAPI_VERSION];
698
            if ('/' !== $baseUrl) {
699
                $docs['servers'] = [['url' => $context[self::BASE_URL] ?? $this->defaultContext[self::BASE_URL]]];
700
            }
701
        } else {
702
            $docs = [
703
                'swagger' => self::SWAGGER_VERSION,
704
                'basePath' => $baseUrl,
705
            ];
706
        }
707
708
        $docs += [
709
            'info' => [
710
                'title' => $documentation->getTitle(),
711
                'version' => $documentation->getVersion(),
712
            ],
713
            'paths' => $paths,
714
        ];
715
716
        if ('' !== $description = $documentation->getDescription()) {
717
            $docs['info']['description'] = $description;
718
        }
719
720
        $securityDefinitions = [];
721
        $security = [];
722
723
        if ($this->oauthEnabled) {
724
            $securityDefinitions['oauth'] = [
725
                'type' => $this->oauthType,
726
                'description' => 'OAuth client_credentials Grant',
727
                'flow' => $this->oauthFlow,
728
                'tokenUrl' => $this->oauthTokenUrl,
729
                'authorizationUrl' => $this->oauthAuthorizationUrl,
730
                'scopes' => $this->oauthScopes,
731
            ];
732
733
            $security[] = ['oauth' => []];
734
        }
735
736
        foreach ($this->apiKeys as $key => $apiKey) {
737
            $name = $apiKey['name'];
738
            $type = $apiKey['type'];
739
740
            $securityDefinitions[$key] = [
741
                'type' => 'apiKey',
742
                'in' => $type,
743
                'description' => sprintf('Value for the %s %s', $name, 'query' === $type ? sprintf('%s parameter', $type) : $type),
744
                'name' => $name,
745
            ];
746
747
            $security[] = [$key => []];
748
        }
749
750
        if ($v3) {
751
            if ($securityDefinitions && $security) {
752
                $docs['security'] = $security;
753
            }
754
        } elseif ($securityDefinitions && $security) {
755
            $docs['securityDefinitions'] = $securityDefinitions;
756
            $docs['security'] = $security;
757
        }
758
759
        if ($v3) {
760
            if (\count($definitions) + \count($securityDefinitions)) {
761
                $docs['components'] = [];
762
                if (\count($definitions)) {
763
                    $docs['components']['schemas'] = $definitions;
764
                }
765
                if (\count($securityDefinitions)) {
766
                    $docs['components']['securitySchemes'] = $securityDefinitions;
767
                }
768
            }
769
        } elseif (\count($definitions) > 0) {
770
            $docs['definitions'] = $definitions;
771
        }
772
773
        return $docs;
774
    }
775
776
    /**
777
     * Gets parameters corresponding to enabled filters.
778
     */
779
    private function getFiltersParameters(bool $v3, string $resourceClass, string $operationName, ResourceMetadata $resourceMetadata, \ArrayObject $definitions, array $serializerContext = null): array
780
    {
781
        if (null === $this->filterLocator) {
782
            return [];
783
        }
784
785
        $parameters = [];
786
        $resourceFilters = $resourceMetadata->getCollectionOperationAttribute($operationName, 'filters', [], true);
787
        foreach ($resourceFilters as $filterId) {
788
            if (!$filter = $this->getFilter($filterId)) {
789
                continue;
790
            }
791
792
            foreach ($filter->getDescription($resourceClass) as $name => $data) {
793
                $parameter = [
794
                    'name' => $name,
795
                    'in' => 'query',
796
                    'required' => $data['required'],
797
                ];
798
799
                $type = $this->getType($v3, $data['type'], $data['is_collection'] ?? false, null, null, $definitions, $serializerContext);
800
                $v3 ? $parameter['schema'] = $type : $parameter += $type;
801
802
                if ('array' === $type['type'] ?? '') {
803
                    $deepObject = \in_array($data['type'], [Type::BUILTIN_TYPE_ARRAY, Type::BUILTIN_TYPE_OBJECT], true);
804
805
                    if ($v3) {
806
                        $parameter['style'] = $deepObject ? 'deepObject' : 'form';
807
                        $parameter['explode'] = true;
808
                    } else {
809
                        $parameter['collectionFormat'] = $deepObject ? 'csv' : 'multi';
810
                    }
811
                }
812
813
                $key = $v3 ? 'openapi' : 'swagger';
814
                if (isset($data[$key])) {
815
                    $parameter = $data[$key] + $parameter;
816
                }
817
818
                $parameters[] = $parameter;
819
            }
820
        }
821
822
        return $parameters;
823
    }
824
825
    /**
826
     * {@inheritdoc}
827
     */
828
    public function supportsNormalization($data, $format = null)
829
    {
830
        return self::FORMAT === $format && $data instanceof Documentation;
831
    }
832
833
    /**
834
     * {@inheritdoc}
835
     */
836
    public function hasCacheableSupportsMethod(): bool
837
    {
838
        return true;
839
    }
840
841
    private function getSerializerContext(string $operationType, bool $denormalization, ResourceMetadata $resourceMetadata, string $operationName): ?array
842
    {
843
        $contextKey = $denormalization ? 'denormalization_context' : 'normalization_context';
844
845
        if (OperationType::COLLECTION === $operationType) {
846
            return $resourceMetadata->getCollectionOperationAttribute($operationName, $contextKey, null, true);
847
        }
848
849
        return $resourceMetadata->getItemOperationAttribute($operationName, $contextKey, null, true);
850
    }
851
852
    private function extractMimeTypes(array $responseFormats): array
853
    {
854
        $responseMimeTypes = [];
855
        foreach ($responseFormats as $mimeTypes) {
856
            foreach ($mimeTypes as $mimeType) {
857
                $responseMimeTypes[] = $mimeType;
858
            }
859
        }
860
861
        return $responseMimeTypes;
862
    }
863
864
    /**
865
     * https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.0.md#linkObject.
866
     */
867
    private function getLinkObject(string $resourceClass, string $operationId, string $path): array
868
    {
869
        $linkObject = $identifiers = [];
870
        foreach ($this->propertyNameCollectionFactory->create($resourceClass, []) as $propertyName) {
871
            $propertyMetadata = $this->propertyMetadataFactory->create($resourceClass, $propertyName);
872
            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...
873
                continue;
874
            }
875
876
            $linkObject['parameters'][$propertyName] = sprintf('$response.body#/%s', $propertyName);
877
            $identifiers[] = $propertyName;
878
        }
879
880
        if (!$linkObject) {
881
            return [];
882
        }
883
        $linkObject['operationId'] = $operationId;
884
        $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);
885
886
        return $linkObject;
887
    }
888
}
889