Completed
Push — master ( ea2ea4...5f7eaf )
by Antoine
01:27 queued 01:20
created

DocumentationNormalizer::normalize()   A

Complexity

Conditions 5
Paths 6

Size

Total Lines 30
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Importance

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

146
            $this->addPaths($v3, $paths, $definitions, $resourceClass, /** @scrutinizer ignore-type */ $resourceShortName, $resourceMetadata, $mimeTypes, OperationType::ITEM, $links);
Loading history...
147
            $this->addPaths($v3, $paths, $definitions, $resourceClass, $resourceShortName, $resourceMetadata, $mimeTypes, OperationType::COLLECTION, $links);
148
149
            if (null === $this->subresourceOperationFactory) {
150
                continue;
151
            }
152
153
            foreach ($this->subresourceOperationFactory->create($resourceClass) as $operationId => $subresourceOperation) {
154
                $paths[$this->getPath($subresourceOperation['shortNames'][0], $subresourceOperation['route_name'], $subresourceOperation, OperationType::SUBRESOURCE)] = $this->addSubresourceOperation($v3, $subresourceOperation, $definitions, $operationId, $mimeTypes, $resourceClass, $resourceMetadata);
155
            }
156
        }
157
158
        $definitions->ksort();
159
        $paths->ksort();
160
161
        return $this->computeDoc($v3, $object, $definitions, $paths, $context);
162
    }
163
164
    /**
165
     * Updates the list of entries in the paths collection.
166
     */
167
    private function addPaths(bool $v3, \ArrayObject $paths, \ArrayObject $definitions, string $resourceClass, string $resourceShortName, ResourceMetadata $resourceMetadata, array $mimeTypes, string $operationType, \ArrayObject $links)
168
    {
169
        if (null === $operations = OperationType::COLLECTION === $operationType ? $resourceMetadata->getCollectionOperations() : $resourceMetadata->getItemOperations()) {
170
            return;
171
        }
172
173
        foreach ($operations as $operationName => $operation) {
174
            $path = $this->getPath($resourceShortName, $operationName, $operation, $operationType);
175
            $method = OperationType::ITEM === $operationType ? $this->operationMethodResolver->getItemOperationMethod($resourceClass, $operationName) : $this->operationMethodResolver->getCollectionOperationMethod($resourceClass, $operationName);
176
177
            $paths[$path][strtolower($method)] = $this->getPathOperation($v3, $operationName, $operation, $method, $operationType, $resourceClass, $resourceMetadata, $mimeTypes, $definitions, $links);
178
        }
179
    }
180
181
    /**
182
     * Gets the path for an operation.
183
     *
184
     * If the path ends with the optional _format parameter, it is removed
185
     * as optional path parameters are not yet supported.
186
     *
187
     * @see https://github.com/OAI/OpenAPI-Specification/issues/93
188
     */
189
    private function getPath(string $resourceShortName, string $operationName, array $operation, string $operationType): string
190
    {
191
        $path = $this->operationPathResolver->resolveOperationPath($resourceShortName, $operation, $operationType, $operationName);
192
        if ('.{_format}' === substr($path, -10)) {
193
            $path = substr($path, 0, -10);
194
        }
195
196
        return $path;
197
    }
198
199
    /**
200
     * Gets a path Operation Object.
201
     *
202
     * @see https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#operation-object
203
     *
204
     * @param string[] $mimeTypes
205
     */
206
    private function getPathOperation(bool $v3, string $operationName, array $operation, string $method, string $operationType, string $resourceClass, ResourceMetadata $resourceMetadata, array $mimeTypes, \ArrayObject $definitions, \ArrayObject $links): \ArrayObject
207
    {
208
        $pathOperation = new \ArrayObject($operation[$v3 ? 'openapi_context' : 'swagger_context'] ?? []);
209
        $resourceShortName = $resourceMetadata->getShortName();
210
        $pathOperation['tags'] ?? $pathOperation['tags'] = [$resourceShortName];
211
        $pathOperation['operationId'] ?? $pathOperation['operationId'] = lcfirst($operationName).ucfirst($resourceShortName).ucfirst($operationType);
212
        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

212
        if ($v3 && 'GET' === $method && OperationType::ITEM === $operationType && $link = $this->getLinkObject($resourceClass, $pathOperation['operationId'], $this->getPath(/** @scrutinizer ignore-type */ $resourceShortName, $operationName, $operation, $operationType))) {
Loading history...
213
            $links[$pathOperation['operationId']] = $link;
214
        }
215
        if ($resourceMetadata->getTypedOperationAttribute($operationType, $operationName, 'deprecation_reason', null, true)) {
216
            $pathOperation['deprecated'] = true;
217
        }
218
        if (null !== $this->formatsProvider) {
219
            $responseFormats = $this->formatsProvider->getFormatsFromOperation($resourceClass, $operationName, $operationType);
220
            $responseMimeTypes = $this->extractMimeTypes($responseFormats);
221
        }
222
        switch ($method) {
223
            case 'GET':
224
                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

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

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

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

233
                return $this->updateDeleteOperation($v3, $pathOperation, /** @scrutinizer ignore-type */ $resourceShortName, $operationType, $operationName, $resourceMetadata);
Loading history...
234
        }
235
236
        return $pathOperation;
237
    }
238
239
    private function updateGetOperation(bool $v3, \ArrayObject $pathOperation, array $mimeTypes, string $operationType, ResourceMetadata $resourceMetadata, string $resourceClass, string $resourceShortName, string $operationName, \ArrayObject $definitions): \ArrayObject
240
    {
241
        $serializerContext = $this->getSerializerContext($operationType, false, $resourceMetadata, $operationName);
242
243
        $responseDefinitionKey = false;
244
        $outputMetadata = $resourceMetadata->getTypedOperationAttribute($operationType, $operationName, 'output', ['class' => $resourceClass], true);
245
        if (null !== $outputClass = $outputMetadata['class'] ?? null) {
246
            $responseDefinitionKey = $this->getDefinition($v3, $definitions, $resourceMetadata, $resourceClass, $outputClass, $serializerContext);
247
        }
248
249
        $successStatus = (string) $resourceMetadata->getTypedOperationAttribute($operationType, $operationName, 'status', '200');
250
251
        if (!$v3) {
252
            $pathOperation['produces'] ?? $pathOperation['produces'] = $mimeTypes;
253
        }
254
255
        if (OperationType::COLLECTION === $operationType) {
256
            $pathOperation['summary'] ?? $pathOperation['summary'] = sprintf('Retrieves the collection of %s resources.', $resourceShortName);
257
258
            $successResponse = ['description' => sprintf('%s collection response', $resourceShortName)];
259
260
            if ($responseDefinitionKey) {
261
                if ($v3) {
262
                    $successResponse['content'] = array_fill_keys($mimeTypes, [
263
                        'schema' => [
264
                            'type' => 'array',
265
                            'items' => ['$ref' => sprintf('#/components/schemas/%s', $responseDefinitionKey)],
266
                        ],
267
                    ]);
268
                } else {
269
                    $successResponse['schema'] = [
270
                        'type' => 'array',
271
                        'items' => ['$ref' => sprintf('#/definitions/%s', $responseDefinitionKey)],
272
                    ];
273
                }
274
            }
275
276
            $pathOperation['responses'] ?? $pathOperation['responses'] = [$successStatus => $successResponse];
277
            $pathOperation['parameters'] ?? $pathOperation['parameters'] = $this->getFiltersParameters($v3, $resourceClass, $operationName, $resourceMetadata, $definitions, $serializerContext);
278
279
            $this->addPaginationParameters($v3, $resourceMetadata, $operationName, $pathOperation);
280
281
            return $pathOperation;
282
        }
283
284
        $pathOperation['summary'] ?? $pathOperation['summary'] = sprintf('Retrieves a %s resource.', $resourceShortName);
285
286
        $parameter = [
287
            'name' => 'id',
288
            'in' => 'path',
289
            'required' => true,
290
        ];
291
        $v3 ? $parameter['schema'] = ['type' => 'string'] : $parameter['type'] = 'string';
292
        $pathOperation['parameters'] ?? $pathOperation['parameters'] = [$parameter];
293
294
        $successResponse = ['description' => sprintf('%s resource response', $resourceShortName)];
295
        if ($responseDefinitionKey) {
296
            if ($v3) {
297
                $successResponse['content'] = array_fill_keys($mimeTypes, ['schema' => ['$ref' => sprintf('#/components/schemas/%s', $responseDefinitionKey)]]);
298
            } else {
299
                $successResponse['schema'] = ['$ref' => sprintf('#/definitions/%s', $responseDefinitionKey)];
300
            }
301
        }
302
303
        $pathOperation['responses'] ?? $pathOperation['responses'] = [
304
            $successStatus => $successResponse,
305
            '404' => ['description' => 'Resource not found'],
306
        ];
307
308
        return $pathOperation;
309
    }
310
311
    private function addPaginationParameters(bool $v3, ResourceMetadata $resourceMetadata, string $operationName, \ArrayObject $pathOperation)
312
    {
313
        if ($this->paginationEnabled && $resourceMetadata->getCollectionOperationAttribute($operationName, 'pagination_enabled', true, true)) {
314
            $paginationParameter = [
315
                'name' => $this->paginationPageParameterName,
316
                'in' => 'query',
317
                'required' => false,
318
                'description' => 'The collection page number',
319
            ];
320
            $v3 ? $paginationParameter['schema'] = ['type' => 'integer'] : $paginationParameter['type'] = 'integer';
321
            $pathOperation['parameters'][] = $paginationParameter;
322
323
            if ($resourceMetadata->getCollectionOperationAttribute($operationName, 'pagination_client_items_per_page', $this->clientItemsPerPage, true)) {
324
                $itemPerPageParameter = [
325
                    'name' => $this->itemsPerPageParameterName,
326
                    'in' => 'query',
327
                    'required' => false,
328
                    'description' => 'The number of items per page',
329
                ];
330
                $v3 ? $itemPerPageParameter['schema'] = ['type' => 'integer'] : $itemPerPageParameter['type'] = 'integer';
331
332
                $pathOperation['parameters'][] = $itemPerPageParameter;
333
            }
334
        }
335
336
        if ($this->paginationEnabled && $resourceMetadata->getCollectionOperationAttribute($operationName, 'pagination_client_enabled', $this->paginationClientEnabled, true)) {
337
            $paginationEnabledParameter = [
338
                'name' => $this->paginationClientEnabledParameterName,
339
                'in' => 'query',
340
                'required' => false,
341
                'description' => 'Enable or disable pagination',
342
            ];
343
            $v3 ? $paginationEnabledParameter['schema'] = ['type' => 'boolean'] : $paginationEnabledParameter['type'] = 'boolean';
344
            $pathOperation['parameters'][] = $paginationEnabledParameter;
345
        }
346
    }
347
348
    /**
349
     * @throws ResourceClassNotFoundException
350
     */
351
    private function addSubresourceOperation(bool $v3, array $subresourceOperation, \ArrayObject $definitions, string $operationId, array $mimeTypes, string $resourceClass, ResourceMetadata $resourceMetadata): \ArrayObject
352
    {
353
        $operationName = 'get'; // TODO: we might want to extract that at some point to also support other subresource operations
354
355
        $subResourceMetadata = $this->resourceMetadataFactory->create($subresourceOperation['resource_class']);
356
        $serializerContext = $this->getSerializerContext(OperationType::SUBRESOURCE, false, $subResourceMetadata, $operationName);
357
        $responseDefinitionKey = $this->getDefinition($v3, $definitions, $subResourceMetadata, $subresourceOperation['resource_class'], null, $serializerContext);
358
        $pathOperation = new \ArrayObject([]);
359
        $pathOperation['tags'] = $subresourceOperation['shortNames'];
360
        $pathOperation['operationId'] = $operationId;
361
        $pathOperation['summary'] = sprintf('Retrieves %s%s resource%s.', $subresourceOperation['collection'] ? 'the collection of ' : 'a ', $subresourceOperation['shortNames'][0], $subresourceOperation['collection'] ? 's' : '');
362
363
        if (null !== $this->formatsProvider) {
364
            $responseFormats = $this->formatsProvider->getFormatsFromOperation($subresourceOperation['resource_class'], $operationName, OperationType::SUBRESOURCE);
365
            $responseMimeTypes = $this->extractMimeTypes($responseFormats);
366
        }
367
368
        if (!$v3) {
369
            $pathOperation['produces'] = $responseMimeTypes ?? $mimeTypes;
370
        }
371
372
        $pathOperation['responses'] = $this->getSubresourceResponse($v3, $responseMimeTypes ?? $mimeTypes, $subresourceOperation['collection'], $subresourceOperation['shortNames'][0], $responseDefinitionKey);
373
        // Avoid duplicates parameters when there is a filter on a subresource identifier
374
        $parametersMemory = [];
375
        $pathOperation['parameters'] = [];
376
        foreach ($subresourceOperation['identifiers'] as list($identifier, , $hasIdentifier)) {
377
            if (true === $hasIdentifier) {
378
                $parameter = ['name' => $identifier, 'in' => 'path', 'required' => true];
379
                $v3 ? $parameter['schema'] = ['type' => 'string'] : $parameter['type'] = 'string';
380
                $pathOperation['parameters'][] = $parameter;
381
                $parametersMemory[] = $identifier;
382
            }
383
        }
384
        if ($parameters = $this->getFiltersParameters($v3, $subresourceOperation['resource_class'], $operationName, $subResourceMetadata, $definitions, $serializerContext)) {
385
            foreach ($parameters as $parameter) {
386
                if (!\in_array($parameter['name'], $parametersMemory, true)) {
387
                    $pathOperation['parameters'][] = $parameter;
388
                }
389
            }
390
        }
391
392
        if ($subresourceOperation['collection']) {
393
            $this->addPaginationParameters($v3, $resourceMetadata, $operationName, $pathOperation);
394
        }
395
396
        return new \ArrayObject(['get' => $pathOperation]);
397
    }
398
399
    private function getSubresourceResponse(bool $v3, $mimeTypes, bool $collection, string $shortName, string $definitionKey): array
400
    {
401
        if ($collection) {
402
            $okResponse = [
403
                'description' => sprintf('%s collection response', $shortName),
404
            ];
405
406
            if ($v3) {
407
                $okResponse['content'] = array_fill_keys($mimeTypes, ['schema' => ['type' => 'array', 'items' => ['$ref' => sprintf('#/components/schemas/%s', $definitionKey)]]]);
408
            } else {
409
                $okResponse['schema'] = ['type' => 'array', 'items' => ['$ref' => sprintf('#/definitions/%s', $definitionKey)]];
410
            }
411
        } else {
412
            $okResponse = [
413
                'description' => sprintf('%s resource response', $shortName),
414
            ];
415
416
            if ($v3) {
417
                $okResponse['content'] = array_fill_keys($mimeTypes, ['schema' => ['$ref' => sprintf('#/components/schemas/%s', $definitionKey)]]);
418
            } else {
419
                $okResponse['schema'] = ['$ref' => sprintf('#/definitions/%s', $definitionKey)];
420
            }
421
        }
422
423
        return ['200' => $okResponse, '404' => ['description' => 'Resource not found']];
424
    }
425
426
    private function updatePostOperation(bool $v3, \ArrayObject $pathOperation, array $mimeTypes, string $operationType, ResourceMetadata $resourceMetadata, string $resourceClass, string $resourceShortName, string $operationName, \ArrayObject $definitions, \ArrayObject $links): \ArrayObject
427
    {
428
        if (!$v3) {
429
            $pathOperation['consumes'] ?? $pathOperation['consumes'] = $mimeTypes;
430
            $pathOperation['produces'] ?? $pathOperation['produces'] = $mimeTypes;
431
        }
432
433
        $pathOperation['summary'] ?? $pathOperation['summary'] = sprintf('Creates a %s resource.', $resourceShortName);
434
435
        $responseDefinitionKey = false;
436
        $outputMetadata = $resourceMetadata->getTypedOperationAttribute($operationType, $operationName, 'output', ['class' => $resourceClass], true);
437
        if (null !== $outputClass = $outputMetadata['class'] ?? null) {
438
            $responseDefinitionKey = $this->getDefinition($v3, $definitions, $resourceMetadata, $resourceClass, $outputClass, $this->getSerializerContext($operationType, false, $resourceMetadata, $operationName));
439
        }
440
441
        $successResponse = ['description' => sprintf('%s resource created', $resourceShortName)];
442
        if ($responseDefinitionKey) {
443
            if ($v3) {
444
                $successResponse['content'] = array_fill_keys($mimeTypes, ['schema' => ['$ref' => sprintf('#/components/schemas/%s', $responseDefinitionKey)]]);
445
                if ($links[$key = 'get'.ucfirst($resourceShortName).ucfirst(OperationType::ITEM)] ?? null) {
446
                    $successResponse['links'] = [ucfirst($key) => $links[$key]];
447
                }
448
            } else {
449
                $successResponse['schema'] = ['$ref' => sprintf('#/definitions/%s', $responseDefinitionKey)];
450
            }
451
        }
452
453
        $pathOperation['responses'] ?? $pathOperation['responses'] = [
454
            (string) $resourceMetadata->getTypedOperationAttribute($operationType, $operationName, 'status', '201') => $successResponse,
455
            '400' => ['description' => 'Invalid input'],
456
            '404' => ['description' => 'Resource not found'],
457
        ];
458
459
        $inputMetadata = $resourceMetadata->getTypedOperationAttribute($operationType, $operationName, 'input', ['class' => $resourceClass], true);
460
        if (null === $inputClass = $inputMetadata['class'] ?? null) {
461
            return $pathOperation;
462
        }
463
464
        $requestDefinitionKey = $this->getDefinition($v3, $definitions, $resourceMetadata, $resourceClass, $inputClass, $this->getSerializerContext($operationType, true, $resourceMetadata, $operationName));
465
        if ($v3) {
466
            $pathOperation['requestBody'] ?? $pathOperation['requestBody'] = [
467
                'content' => array_fill_keys($mimeTypes, ['schema' => ['$ref' => sprintf('#/components/schemas/%s', $requestDefinitionKey)]]),
468
                'description' => sprintf('The new %s resource', $resourceShortName),
469
            ];
470
        } else {
471
            $pathOperation['parameters'] ?? $pathOperation['parameters'] = [[
472
                'name' => lcfirst($resourceShortName),
473
                'in' => 'body',
474
                'description' => sprintf('The new %s resource', $resourceShortName),
475
                'schema' => ['$ref' => sprintf('#/definitions/%s', $requestDefinitionKey)],
476
            ]];
477
        }
478
479
        return $pathOperation;
480
    }
481
482
    private function updatePutOperation(bool $v3, \ArrayObject $pathOperation, array $mimeTypes, string $operationType, ResourceMetadata $resourceMetadata, string $resourceClass, string $resourceShortName, string $operationName, \ArrayObject $definitions): \ArrayObject
483
    {
484
        if (!$v3) {
485
            $pathOperation['consumes'] ?? $pathOperation['consumes'] = $mimeTypes;
486
            $pathOperation['produces'] ?? $pathOperation['produces'] = $mimeTypes;
487
        }
488
489
        $pathOperation['summary'] ?? $pathOperation['summary'] = sprintf('Replaces the %s resource.', $resourceShortName);
490
491
        $parameter = [
492
            'name' => 'id',
493
            'in' => 'path',
494
            'required' => true,
495
        ];
496
        $v3 ? $parameter['schema'] = ['type' => 'string'] : $parameter['type'] = 'string';
497
        $pathOperation['parameters'] ?? $pathOperation['parameters'] = [$parameter];
498
499
        $responseDefinitionKey = false;
500
        $outputMetadata = $resourceMetadata->getTypedOperationAttribute($operationType, $operationName, 'output', ['class' => $resourceClass], true);
501
        if (null !== $outputClass = $outputMetadata['class'] ?? null) {
502
            $responseDefinitionKey = $this->getDefinition($v3, $definitions, $resourceMetadata, $resourceClass, $outputClass, $this->getSerializerContext($operationType, false, $resourceMetadata, $operationName));
503
        }
504
505
        $successResponse = ['description' => sprintf('%s resource updated', $resourceShortName)];
506
        if ($responseDefinitionKey) {
507
            if ($v3) {
508
                $successResponse['content'] = array_fill_keys($mimeTypes, ['schema' => ['$ref' => sprintf('#/components/schemas/%s', $responseDefinitionKey)]]);
509
            } else {
510
                $successResponse['schema'] = ['$ref' => sprintf('#/definitions/%s', $responseDefinitionKey)];
511
            }
512
        }
513
514
        $pathOperation['responses'] ?? $pathOperation['responses'] = [
515
            (string) $resourceMetadata->getTypedOperationAttribute($operationType, $operationName, 'status', '200') => $successResponse,
516
            '400' => ['description' => 'Invalid input'],
517
            '404' => ['description' => 'Resource not found'],
518
        ];
519
520
        $inputMetadata = $resourceMetadata->getTypedOperationAttribute($operationType, $operationName, 'input', ['class' => $resourceClass], true);
521
        if (null === $inputClass = $inputMetadata['class'] ?? null) {
522
            return $pathOperation;
523
        }
524
525
        $requestDefinitionKey = $this->getDefinition($v3, $definitions, $resourceMetadata, $resourceClass, $inputClass, $this->getSerializerContext($operationType, true, $resourceMetadata, $operationName));
526
        if ($v3) {
527
            $pathOperation['requestBody'] ?? $pathOperation['requestBody'] = [
528
                'content' => array_fill_keys($mimeTypes, ['schema' => ['$ref' => sprintf('#/components/schemas/%s', $requestDefinitionKey)]]),
529
                'description' => sprintf('The updated %s resource', $resourceShortName),
530
            ];
531
        } else {
532
            $pathOperation['parameters'][] = [
533
                'name' => lcfirst($resourceShortName),
534
                'in' => 'body',
535
                'description' => sprintf('The updated %s resource', $resourceShortName),
536
                'schema' => ['$ref' => sprintf('#/definitions/%s', $requestDefinitionKey)],
537
            ];
538
        }
539
540
        return $pathOperation;
541
    }
542
543
    private function updateDeleteOperation(bool $v3, \ArrayObject $pathOperation, string $resourceShortName, string $operationType, string $operationName, ResourceMetadata $resourceMetadata): \ArrayObject
544
    {
545
        $pathOperation['summary'] ?? $pathOperation['summary'] = sprintf('Removes the %s resource.', $resourceShortName);
546
        $pathOperation['responses'] ?? $pathOperation['responses'] = [
547
            (string) $resourceMetadata->getTypedOperationAttribute($operationType, $operationName, 'status', '204') => ['description' => sprintf('%s resource deleted', $resourceShortName)],
548
            '404' => ['description' => 'Resource not found'],
549
        ];
550
551
        $parameter = [
552
            'name' => 'id',
553
            'in' => 'path',
554
            'required' => true,
555
        ];
556
        $v3 ? $parameter['schema'] = ['type' => 'string'] : $parameter['type'] = 'string';
557
558
        $pathOperation['parameters'] ?? $pathOperation['parameters'] = [$parameter];
559
560
        return $pathOperation;
561
    }
562
563
    private function getDefinition(bool $v3, \ArrayObject $definitions, ResourceMetadata $resourceMetadata, string $resourceClass, ?string $publicClass, array $serializerContext = null): string
564
    {
565
        $keyPrefix = $resourceMetadata->getShortName();
566
        if (null !== $publicClass && $resourceClass !== $publicClass) {
567
            $keyPrefix .= ':'.md5($publicClass);
568
        }
569
570
        if (isset($serializerContext[self::SWAGGER_DEFINITION_NAME])) {
571
            $definitionKey = sprintf('%s-%s', $keyPrefix, $serializerContext[self::SWAGGER_DEFINITION_NAME]);
572
        } else {
573
            $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

573
            $definitionKey = $this->getDefinitionKey(/** @scrutinizer ignore-type */ $keyPrefix, (array) ($serializerContext[AbstractNormalizer::GROUPS] ?? []));
Loading history...
574
        }
575
576
        if (!isset($definitions[$definitionKey])) {
577
            $definitions[$definitionKey] = [];  // Initialize first to prevent infinite loop
578
            $definitions[$definitionKey] = $this->getDefinitionSchema($v3, $publicClass ?? $resourceClass, $resourceMetadata, $definitions, $serializerContext);
579
        }
580
581
        return $definitionKey;
582
    }
583
584
    private function getDefinitionKey(string $resourceShortName, array $groups): string
585
    {
586
        return $groups ? sprintf('%s-%s', $resourceShortName, implode('_', $groups)) : $resourceShortName;
587
    }
588
589
    /**
590
     * Gets a definition Schema Object.
591
     *
592
     * @see https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#schemaObject
593
     */
594
    private function getDefinitionSchema(bool $v3, string $resourceClass, ResourceMetadata $resourceMetadata, \ArrayObject $definitions, array $serializerContext = null): \ArrayObject
595
    {
596
        $definitionSchema = new \ArrayObject(['type' => 'object']);
597
598
        if (null !== $description = $resourceMetadata->getDescription()) {
599
            $definitionSchema['description'] = $description;
600
        }
601
602
        if (null !== $iri = $resourceMetadata->getIri()) {
603
            $definitionSchema['externalDocs'] = ['url' => $iri];
604
        }
605
606
        $options = isset($serializerContext[AbstractNormalizer::GROUPS]) ? ['serializer_groups' => $serializerContext[AbstractNormalizer::GROUPS]] : [];
607
        foreach ($this->propertyNameCollectionFactory->create($resourceClass, $options) as $propertyName) {
608
            $propertyMetadata = $this->propertyMetadataFactory->create($resourceClass, $propertyName);
609
            $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

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