vendor/api-platform/core/src/Core/Swagger/Serializer/DocumentationNormalizer.php line 124

Open in your IDE?
  1. <?php
  2. /*
  3.  * This file is part of the API Platform project.
  4.  *
  5.  * (c) Kévin Dunglas <dunglas@gmail.com>
  6.  *
  7.  * For the full copyright and license information, please view the LICENSE
  8.  * file that was distributed with this source code.
  9.  */
  10. declare(strict_types=1);
  11. namespace ApiPlatform\Core\Swagger\Serializer;
  12. use ApiPlatform\Core\Api\FilterCollection;
  13. use ApiPlatform\Core\Api\FilterLocatorTrait;
  14. use ApiPlatform\Core\Api\FormatsProviderInterface;
  15. use ApiPlatform\Core\Api\IdentifiersExtractorInterface;
  16. use ApiPlatform\Core\Api\OperationAwareFormatsProviderInterface;
  17. use ApiPlatform\Core\Api\OperationMethodResolverInterface;
  18. use ApiPlatform\Core\Api\OperationType;
  19. use ApiPlatform\Core\Api\ResourceClassResolverInterface;
  20. use ApiPlatform\Core\Api\UrlGeneratorInterface;
  21. use ApiPlatform\Core\JsonSchema\SchemaFactory as LegacySchemaFactory;
  22. use ApiPlatform\Core\JsonSchema\SchemaFactoryInterface as LegacySchemaFactoryInterface;
  23. use ApiPlatform\Core\Metadata\Property\Factory\PropertyMetadataFactoryInterface;
  24. use ApiPlatform\Core\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface;
  25. use ApiPlatform\Core\Metadata\Resource\ApiResourceToLegacyResourceMetadataTrait;
  26. use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface;
  27. use ApiPlatform\Core\Metadata\Resource\ResourceMetadata;
  28. use ApiPlatform\Core\Operation\Factory\SubresourceOperationFactoryInterface;
  29. use ApiPlatform\Documentation\Documentation;
  30. use ApiPlatform\Exception\ResourceClassNotFoundException;
  31. use ApiPlatform\Exception\RuntimeException;
  32. use ApiPlatform\JsonSchema\Schema;
  33. use ApiPlatform\JsonSchema\SchemaFactory;
  34. use ApiPlatform\JsonSchema\SchemaFactoryInterface;
  35. use ApiPlatform\JsonSchema\TypeFactory;
  36. use ApiPlatform\JsonSchema\TypeFactoryInterface;
  37. use ApiPlatform\Metadata\HttpOperation;
  38. use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface;
  39. use ApiPlatform\OpenApi\OpenApi;
  40. use ApiPlatform\OpenApi\Serializer\ApiGatewayNormalizer;
  41. use ApiPlatform\PathResolver\OperationPathResolverInterface;
  42. use Psr\Container\ContainerInterface;
  43. use Symfony\Component\PropertyInfo\Type;
  44. use Symfony\Component\Serializer\NameConverter\NameConverterInterface;
  45. use Symfony\Component\Serializer\Normalizer\CacheableSupportsMethodInterface;
  46. use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
  47. /**
  48.  * Generates an OpenAPI specification (formerly known as Swagger). OpenAPI v2 and v3 are supported.
  49.  *
  50.  * @author Amrouche Hamza <hamza.simperfit@gmail.com>
  51.  * @author Teoh Han Hui <teohhanhui@gmail.com>
  52.  * @author Kévin Dunglas <dunglas@gmail.com>
  53.  * @author Anthony GRASSIOT <antograssiot@free.fr>
  54.  */
  55. final class DocumentationNormalizer implements NormalizerInterface, CacheableSupportsMethodInterface
  56. {
  57.     use ApiResourceToLegacyResourceMetadataTrait;
  58.     use FilterLocatorTrait;
  59.     public const FORMAT = 'json';
  60.     public const BASE_URL = 'base_url';
  61.     public const SPEC_VERSION = 'spec_version';
  62.     public const OPENAPI_VERSION = '3.0.2';
  63.     public const SWAGGER_DEFINITION_NAME = 'swagger_definition_name';
  64.     public const SWAGGER_VERSION = '2.0';
  65.     /**
  66.      * @deprecated
  67.      */
  68.     public const ATTRIBUTE_NAME = 'swagger_context';
  69.     private $resourceMetadataFactory;
  70.     private $propertyNameCollectionFactory;
  71.     private $propertyMetadataFactory;
  72.     private $operationMethodResolver;
  73.     private $operationPathResolver;
  74.     private $oauthEnabled;
  75.     private $oauthType;
  76.     private $oauthFlow;
  77.     private $oauthTokenUrl;
  78.     private $oauthAuthorizationUrl;
  79.     private $oauthScopes;
  80.     private $apiKeys;
  81.     private $subresourceOperationFactory;
  82.     private $paginationEnabled;
  83.     private $paginationPageParameterName;
  84.     private $clientItemsPerPage;
  85.     private $itemsPerPageParameterName;
  86.     private $paginationClientEnabled;
  87.     private $paginationClientEnabledParameterName;
  88.     private $formats;
  89.     private $formatsProvider;
  90.     /**
  91.      * @var SchemaFactoryInterface|LegacySchemaFactoryInterface
  92.      */
  93.     private $jsonSchemaFactory;
  94.     /**
  95.      * @var TypeFactoryInterface
  96.      */
  97.     private $jsonSchemaTypeFactory;
  98.     private $defaultContext = [
  99.         self::BASE_URL => '/',
  100.         ApiGatewayNormalizer::API_GATEWAY => false,
  101.     ];
  102.     private $identifiersExtractor;
  103.     private $openApiNormalizer;
  104.     private $legacyMode;
  105.     /**
  106.      * @param LegacySchemaFactoryInterface|SchemaFactoryInterface|ResourceClassResolverInterface|null $jsonSchemaFactory
  107.      * @param ContainerInterface|FilterCollection|null                                                $filterLocator
  108.      * @param array|OperationAwareFormatsProviderInterface                                            $formats
  109.      * @param mixed|null                                                                              $jsonSchemaTypeFactory
  110.      * @param int[]                                                                                   $swaggerVersions
  111.      */
  112.     public function __construct($resourceMetadataFactory, PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, $jsonSchemaFactory = null, $jsonSchemaTypeFactory = null, OperationPathResolverInterface $operationPathResolver = null, UrlGeneratorInterface $urlGenerator = null, $filterLocator = null, NameConverterInterface $nameConverter = null, bool $oauthEnabled = false, string $oauthType = '', string $oauthFlow = '', string $oauthTokenUrl = '', string $oauthAuthorizationUrl = '', array $oauthScopes = [], array $apiKeys = [], SubresourceOperationFactoryInterface $subresourceOperationFactory = null, bool $paginationEnabled = true, string $paginationPageParameterName = 'page', bool $clientItemsPerPage = false, string $itemsPerPageParameterName = 'itemsPerPage', $formats = [], bool $paginationClientEnabled = false, string $paginationClientEnabledParameterName = 'pagination', array $defaultContext = [], array $swaggerVersions = [2, 3], IdentifiersExtractorInterface $identifiersExtractor = null, NormalizerInterface $openApiNormalizer = null, bool $legacyMode = false)
  113.     {
  114.         if ($jsonSchemaTypeFactory instanceof OperationMethodResolverInterface) {
  115.             @trigger_error(sprintf('Passing an instance of %s to %s() is deprecated since version 2.5 and will be removed in 3.0.', OperationMethodResolverInterface::class, __METHOD__), \E_USER_DEPRECATED);
  116.             $this->operationMethodResolver = $jsonSchemaTypeFactory;
  117.             $this->jsonSchemaTypeFactory = new TypeFactory();
  118.         } else {
  119.             $this->jsonSchemaTypeFactory = $jsonSchemaTypeFactory ?? new TypeFactory();
  120.         }
  121.         if ($jsonSchemaFactory instanceof ResourceClassResolverInterface) {
  122.             @trigger_error(sprintf('Passing an instance of %s to %s() is deprecated since version 2.5 and will be removed in 3.0.', ResourceClassResolverInterface::class, __METHOD__), \E_USER_DEPRECATED);
  123.         }
  124.         if (null === $jsonSchemaFactory || $jsonSchemaFactory instanceof ResourceClassResolverInterface) {
  125.             if ($resourceMetadataFactory instanceof ResourceMetadataFactoryInterface) {
  126.                 $jsonSchemaFactory = new LegacySchemaFactory($this->jsonSchemaTypeFactory, $resourceMetadataFactory, $propertyNameCollectionFactory, $propertyMetadataFactory, $nameConverter);
  127.             } else {
  128.                 $jsonSchemaFactory = new SchemaFactory($this->jsonSchemaTypeFactory, $resourceMetadataFactory, $propertyNameCollectionFactory, $propertyMetadataFactory, $nameConverter);
  129.             }
  130.             $this->jsonSchemaTypeFactory->setSchemaFactory($jsonSchemaFactory);
  131.         }
  132.         $this->jsonSchemaFactory = $jsonSchemaFactory;
  133.         if ($nameConverter) {
  134.             @trigger_error(sprintf('Passing an instance of %s to %s() is deprecated since version 2.5 and will be removed in 3.0.', NameConverterInterface::class, __METHOD__), \E_USER_DEPRECATED);
  135.         }
  136.         if ($urlGenerator) {
  137.             @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);
  138.         }
  139.         if ($formats instanceof FormatsProviderInterface) {
  140.             @trigger_error(sprintf('Passing an instance of %s to %s() is deprecated since version 2.5 and will be removed in 3.0, pass an array instead.', FormatsProviderInterface::class, __METHOD__), \E_USER_DEPRECATED);
  141.             $this->formatsProvider = $formats;
  142.         } else {
  143.             $this->formats = $formats;
  144.         }
  145.         $this->setFilterLocator($filterLocator, true);
  146.         if ($resourceMetadataFactory instanceof ResourceMetadataFactoryInterface) {
  147.             trigger_deprecation('api-platform/core', '2.7', sprintf('Use "%s" instead of "%s".', ResourceMetadataCollectionFactoryInterface::class, ResourceMetadataFactoryInterface::class));
  148.         }
  149.         $this->resourceMetadataFactory = $resourceMetadataFactory;
  150.         $this->propertyNameCollectionFactory = $propertyNameCollectionFactory;
  151.         $this->propertyMetadataFactory = $propertyMetadataFactory;
  152.         $this->operationPathResolver = $operationPathResolver;
  153.         $this->oauthEnabled = $oauthEnabled;
  154.         $this->oauthType = $oauthType;
  155.         $this->oauthFlow = $oauthFlow;
  156.         $this->oauthTokenUrl = $oauthTokenUrl;
  157.         $this->oauthAuthorizationUrl = $oauthAuthorizationUrl;
  158.         $this->oauthScopes = $oauthScopes;
  159.         $this->subresourceOperationFactory = $subresourceOperationFactory;
  160.         $this->paginationEnabled = $paginationEnabled;
  161.         $this->paginationPageParameterName = $paginationPageParameterName;
  162.         $this->apiKeys = $apiKeys;
  163.         $this->clientItemsPerPage = $clientItemsPerPage;
  164.         $this->itemsPerPageParameterName = $itemsPerPageParameterName;
  165.         $this->paginationClientEnabled = $paginationClientEnabled;
  166.         $this->paginationClientEnabledParameterName = $paginationClientEnabledParameterName;
  167.         $this->defaultContext[self::SPEC_VERSION] = $swaggerVersions[0] ?? 2;
  168.         $this->defaultContext = array_merge($this->defaultContext, $defaultContext);
  169.         $this->identifiersExtractor = $identifiersExtractor;
  170.         $this->openApiNormalizer = $openApiNormalizer;
  171.         $this->legacyMode = $legacyMode;
  172.     }
  173.     /**
  174.      * @param mixed|null $format
  175.      *
  176.      * @return array|string|int|float|bool|\ArrayObject|null
  177.      */
  178.     public function normalize($object, $format = null, array $context = [])
  179.     {
  180.         if ($object instanceof OpenApi) {
  181.             @trigger_error('Using the swagger DocumentationNormalizer is deprecated in favor of decorating the OpenApiFactory, use the "openapi.backward_compatibility_layer" configuration to change this behavior.', \E_USER_DEPRECATED);
  182.             return $this->openApiNormalizer->normalize($object, $format, $context);
  183.         }
  184.         $v3 = 3 === ($context['spec_version'] ?? $this->defaultContext['spec_version']) && !($context['api_gateway'] ?? $this->defaultContext['api_gateway']);
  185.         $definitions = new \ArrayObject();
  186.         $paths = new \ArrayObject();
  187.         $links = new \ArrayObject();
  188.         if ($this->resourceMetadataFactory instanceof ResourceMetadataCollectionFactoryInterface) {
  189.             foreach ($object->getResourceNameCollection() as $resourceClass) {
  190.                 $resourceMetadataCollection = $this->resourceMetadataFactory->create($resourceClass);
  191.                 foreach ($resourceMetadataCollection as $i => $resourceMetadata) {
  192.                     $resourceMetadata = $this->transformResourceToResourceMetadata($resourceMetadata);
  193.                     // Items needs to be parsed first to be able to reference the lines from the collection operation
  194.                     $this->addPaths($v3, $paths, $definitions, $resourceClass, $resourceMetadata->getShortName(), $resourceMetadata, OperationType::ITEM, $links);
  195.                     $this->addPaths($v3, $paths, $definitions, $resourceClass, $resourceMetadata->getShortName(), $resourceMetadata, OperationType::COLLECTION, $links);
  196.                 }
  197.             }
  198.             $definitions->ksort();
  199.             $paths->ksort();
  200.             return $this->computeDoc($v3, $object, $definitions, $paths, $context);
  201.         }
  202.         foreach ($object->getResourceNameCollection() as $resourceClass) {
  203.             $resourceMetadata = $this->resourceMetadataFactory->create($resourceClass);
  204.             if ($this->identifiersExtractor) {
  205.                 $identifiers = [];
  206.                 if ($resourceMetadata->getItemOperations()) {
  207.                     $identifiers = $this->identifiersExtractor->getIdentifiersFromResourceClass($resourceClass);
  208.                 }
  209.                 $resourceMetadata = $resourceMetadata->withAttributes(($resourceMetadata->getAttributes() ?: []) + ['identifiers' => $identifiers]);
  210.             }
  211.             $resourceShortName = $resourceMetadata->getShortName();
  212.             // Items needs to be parsed first to be able to reference the lines from the collection operation
  213.             $this->addPaths($v3, $paths, $definitions, $resourceClass, $resourceShortName, $resourceMetadata, OperationType::ITEM, $links);
  214.             $this->addPaths($v3, $paths, $definitions, $resourceClass, $resourceShortName, $resourceMetadata, OperationType::COLLECTION, $links);
  215.             if (null === $this->subresourceOperationFactory) {
  216.                 continue;
  217.             }
  218.             foreach ($this->subresourceOperationFactory->create($resourceClass) as $operationId => $subresourceOperation) {
  219.                 $method = $resourceMetadata->getTypedOperationAttribute(OperationType::SUBRESOURCE, $subresourceOperation['operation_name'], 'method', 'GET');
  220.                 $paths[$this->getPath($subresourceOperation['shortNames'][0], $subresourceOperation['route_name'], $subresourceOperation, OperationType::SUBRESOURCE)][strtolower($method)] = $this->addSubresourceOperation($v3, $subresourceOperation, $definitions, $operationId, $resourceMetadata);
  221.             }
  222.         }
  223.         $definitions->ksort();
  224.         $paths->ksort();
  225.         return $this->computeDoc($v3, $object, $definitions, $paths, $context);
  226.     }
  227.     /**
  228.      * Updates the list of entries in the paths collection.
  229.      */
  230.     private function addPaths(bool $v3, \ArrayObject $paths, \ArrayObject $definitions, string $resourceClass, string $resourceShortName, ResourceMetadata $resourceMetadata, string $operationType, \ArrayObject $links)
  231.     {
  232.         if (null === $operations = OperationType::COLLECTION === $operationType ? $resourceMetadata->getCollectionOperations() : $resourceMetadata->getItemOperations()) {
  233.             return;
  234.         }
  235.         foreach ($operations as $operationName => $operation) {
  236.             if (false === ($operation['openapi'] ?? null)) {
  237.                 continue;
  238.             }
  239.             // Skolem IRI
  240.             if ('api_genid' === ($operation['route_name'] ?? null)) {
  241.                 continue;
  242.             }
  243.             if (isset($operation['uri_template'])) {
  244.                 $path = str_replace('.{_format}', '', $operation['uri_template']);
  245.                 if (!str_starts_with($path, '/')) {
  246.                     $path = '/'.$path;
  247.                 }
  248.             } else {
  249.                 $path = $this->getPath($resourceShortName, $operationName, $operation, $operationType);
  250.             }
  251.             if ($this->operationMethodResolver) {
  252.                 $method = OperationType::ITEM === $operationType ? $this->operationMethodResolver->getItemOperationMethod($resourceClass, $operationName) : $this->operationMethodResolver->getCollectionOperationMethod($resourceClass, $operationName);
  253.             } else {
  254.                 $method = $resourceMetadata->getTypedOperationAttribute($operationType, $operationName, 'method', 'GET');
  255.             }
  256.             $paths[$path][strtolower($method)] = $this->getPathOperation($v3, $operationName, $operation, $method, $operationType, $resourceClass, $resourceMetadata, $definitions, $links);
  257.         }
  258.     }
  259.     /**
  260.      * Gets the path for an operation.
  261.      *
  262.      * If the path ends with the optional _format parameter, it is removed
  263.      * as optional path parameters are not yet supported.
  264.      *
  265.      * @see https://github.com/OAI/OpenAPI-Specification/issues/93
  266.      */
  267.     private function getPath(string $resourceShortName, string $operationName, array $operation, string $operationType): string
  268.     {
  269.         $path = $this->operationPathResolver->resolveOperationPath($resourceShortName, $operation, $operationType, $operationName);
  270.         if ('.{_format}' === substr($path, -10)) {
  271.             $path = substr($path, 0, -10);
  272.         }
  273.         return $path;
  274.     }
  275.     /**
  276.      * Gets a path Operation Object.
  277.      *
  278.      * @see https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#operation-object
  279.      */
  280.     private function getPathOperation(bool $v3, string $operationName, array $operation, string $method, string $operationType, string $resourceClass, ResourceMetadata $resourceMetadata, \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))) {
  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.             $requestFormats = $resourceMetadata->getTypedOperationAttribute($operationType, $operationName, 'input_formats', [], true);
  294.             $responseFormats = $resourceMetadata->getTypedOperationAttribute($operationType, $operationName, 'output_formats', [], true);
  295.         } else {
  296.             $requestFormats = $responseFormats = $this->formatsProvider->getFormatsFromOperation($resourceClass, $operationName, $operationType);
  297.         }
  298.         $requestMimeTypes = $this->flattenMimeTypes($requestFormats);
  299.         $responseMimeTypes = $this->flattenMimeTypes($responseFormats);
  300.         switch ($method) {
  301.             case 'GET':
  302.                 return $this->updateGetOperation($v3, $pathOperation, $responseMimeTypes, $operationType, $resourceMetadata, $resourceClass, $resourceShortName, $operationName, $definitions);
  303.             case 'POST':
  304.                 return $this->updatePostOperation($v3, $pathOperation, $requestMimeTypes, $responseMimeTypes, $operationType, $resourceMetadata, $resourceClass, $resourceShortName, $operationName, $definitions, $links);
  305.             case 'PATCH':
  306.                 $pathOperation['summary'] ?? $pathOperation['summary'] = sprintf('Updates the %s resource.', $resourceShortName);
  307.                 // no break
  308.             case 'PUT':
  309.                 return $this->updatePutOperation($v3, $pathOperation, $requestMimeTypes, $responseMimeTypes, $operationType, $resourceMetadata, $resourceClass, $resourceShortName, $operationName, $definitions);
  310.             case 'DELETE':
  311.                 return $this->updateDeleteOperation($v3, $pathOperation, $resourceShortName, $operationType, $operationName, $resourceMetadata, $resourceClass);
  312.         }
  313.         return $pathOperation;
  314.     }
  315.     /**
  316.      * @return array the update message as first value, and if the schema is defined as second
  317.      */
  318.     private function addSchemas(bool $v3, array $message, \ArrayObject $definitions, string $resourceClass, string $operationType, string $operationName, array $mimeTypes, string $type = Schema::TYPE_OUTPUT, bool $forceCollection = false): array
  319.     {
  320.         if (!$v3) {
  321.             $jsonSchema = $this->getJsonSchema($v3, $definitions, $resourceClass, $type, $operationType, $operationName, 'json', null, $forceCollection);
  322.             if (!$jsonSchema->isDefined()) {
  323.                 return [$message, false];
  324.             }
  325.             $message['schema'] = $jsonSchema->getArrayCopy(false);
  326.             return [$message, true];
  327.         }
  328.         foreach ($mimeTypes as $mimeType => $format) {
  329.             $jsonSchema = $this->getJsonSchema($v3, $definitions, $resourceClass, $type, $operationType, $operationName, $format, null, $forceCollection);
  330.             if (!$jsonSchema->isDefined()) {
  331.                 return [$message, false];
  332.             }
  333.             $message['content'][$mimeType] = ['schema' => $jsonSchema->getArrayCopy(false)];
  334.         }
  335.         return [$message, true];
  336.     }
  337.     private function updateGetOperation(bool $v3, \ArrayObject $pathOperation, array $mimeTypes, string $operationType, ResourceMetadata $resourceMetadata, string $resourceClass, string $resourceShortName, string $operationName, \ArrayObject $definitions): \ArrayObject
  338.     {
  339.         $successStatus = (string) $resourceMetadata->getTypedOperationAttribute($operationType, $operationName, 'status', '200');
  340.         if (!$v3) {
  341.             $pathOperation['produces'] ?? $pathOperation['produces'] = array_keys($mimeTypes);
  342.         }
  343.         if (OperationType::COLLECTION === $operationType) {
  344.             $outputResourseShortName = $resourceMetadata->getCollectionOperations()[$operationName]['output']['name'] ?? $resourceShortName;
  345.             $pathOperation['summary'] ?? $pathOperation['summary'] = sprintf('Retrieves the collection of %s resources.', $outputResourseShortName);
  346.             $successResponse = ['description' => sprintf('%s collection response', $outputResourseShortName)];
  347.             [$successResponse] = $this->addSchemas($v3, $successResponse, $definitions, $resourceClass, $operationType, $operationName, $mimeTypes);
  348.             $pathOperation['responses'] ?? $pathOperation['responses'] = [$successStatus => $successResponse];
  349.             if (
  350.                 ($resourceMetadata->getAttributes()['extra_properties']['is_legacy_subresource'] ?? false)
  351.                 || ($resourceMetadata->getAttributes()['extra_properties']['is_alternate_resource_metadata'] ?? false)) {
  352.                 // Avoid duplicates parameters when there is a filter on a subresource identifier
  353.                 $parametersMemory = [];
  354.                 $pathOperation['parameters'] = [];
  355.                 foreach ($resourceMetadata->getCollectionOperations()[$operationName]['identifiers'] as $parameterName => [$class, $identifier]) {
  356.                     $parameter = ['name' => $parameterName, 'in' => 'path', 'required' => true];
  357.                     $v3 ? $parameter['schema'] = ['type' => 'string'] : $parameter['type'] = 'string';
  358.                     $pathOperation['parameters'][] = $parameter;
  359.                     $parametersMemory[] = $parameterName;
  360.                 }
  361.                 if ($parameters = $this->getFiltersParameters($v3, $resourceClass, $operationName, $resourceMetadata)) {
  362.                     foreach ($parameters as $parameter) {
  363.                         if (!\in_array($parameter['name'], $parametersMemory, true)) {
  364.                             $pathOperation['parameters'][] = $parameter;
  365.                         }
  366.                     }
  367.                 }
  368.             } else {
  369.                 $pathOperation['parameters'] ?? $pathOperation['parameters'] = $this->getFiltersParameters($v3, $resourceClass, $operationName, $resourceMetadata);
  370.             }
  371.             $this->addPaginationParameters($v3, $resourceMetadata, OperationType::COLLECTION, $operationName, $pathOperation);
  372.             return $pathOperation;
  373.         }
  374.         $outputResourseShortName = $resourceMetadata->getItemOperations()[$operationName]['output']['name'] ?? $resourceShortName;
  375.         $pathOperation['summary'] ?? $pathOperation['summary'] = sprintf('Retrieves a %s resource.', $outputResourseShortName);
  376.         $pathOperation = $this->addItemOperationParameters($v3, $pathOperation, $operationType, $operationName, $resourceMetadata, $resourceClass);
  377.         $successResponse = ['description' => sprintf('%s resource response', $outputResourseShortName)];
  378.         [$successResponse] = $this->addSchemas($v3, $successResponse, $definitions, $resourceClass, $operationType, $operationName, $mimeTypes);
  379.         $pathOperation['responses'] ?? $pathOperation['responses'] = [
  380.             $successStatus => $successResponse,
  381.             '404' => ['description' => 'Resource not found'],
  382.         ];
  383.         return $pathOperation;
  384.     }
  385.     private function addPaginationParameters(bool $v3, ResourceMetadata $resourceMetadata, string $operationType, string $operationName, \ArrayObject $pathOperation)
  386.     {
  387.         if ($this->paginationEnabled && $resourceMetadata->getTypedOperationAttribute($operationType, $operationName, 'pagination_enabled', true, true)) {
  388.             $paginationParameter = [
  389.                 'name' => $this->paginationPageParameterName,
  390.                 'in' => 'query',
  391.                 'required' => false,
  392.                 'description' => 'The collection page number',
  393.             ];
  394.             $v3 ? $paginationParameter['schema'] = [
  395.                 'type' => 'integer',
  396.                 'default' => 1,
  397.             ] : $paginationParameter['type'] = 'integer';
  398.             $pathOperation['parameters'][] = $paginationParameter;
  399.             if ($resourceMetadata->getTypedOperationAttribute($operationType, $operationName, 'pagination_client_items_per_page', $this->clientItemsPerPage, true)) {
  400.                 $itemPerPageParameter = [
  401.                     'name' => $this->itemsPerPageParameterName,
  402.                     'in' => 'query',
  403.                     'required' => false,
  404.                     'description' => 'The number of items per page',
  405.                 ];
  406.                 if ($v3) {
  407.                     $itemPerPageParameter['schema'] = [
  408.                         'type' => 'integer',
  409.                         'default' => $resourceMetadata->getTypedOperationAttribute($operationType, $operationName, 'pagination_items_per_page', 30, true),
  410.                         'minimum' => 0,
  411.                     ];
  412.                     $maxItemsPerPage = $resourceMetadata->getTypedOperationAttribute($operationType, $operationName, 'maximum_items_per_page', null, true);
  413.                     if (null !== $maxItemsPerPage) {
  414.                         @trigger_error('The "maximum_items_per_page" option has been deprecated since API Platform 2.5 in favor of "pagination_maximum_items_per_page" and will be removed in API Platform 3.', \E_USER_DEPRECATED);
  415.                     }
  416.                     $maxItemsPerPage = $resourceMetadata->getTypedOperationAttribute($operationType, $operationName, 'pagination_maximum_items_per_page', $maxItemsPerPage, true);
  417.                     if (null !== $maxItemsPerPage) {
  418.                         $itemPerPageParameter['schema']['maximum'] = $maxItemsPerPage;
  419.                     }
  420.                 } else {
  421.                     $itemPerPageParameter['type'] = 'integer';
  422.                 }
  423.                 $pathOperation['parameters'][] = $itemPerPageParameter;
  424.             }
  425.         }
  426.         if ($this->paginationEnabled && $resourceMetadata->getTypedOperationAttribute($operationType, $operationName, 'pagination_client_enabled', $this->paginationClientEnabled, true)) {
  427.             $paginationEnabledParameter = [
  428.                 'name' => $this->paginationClientEnabledParameterName,
  429.                 'in' => 'query',
  430.                 'required' => false,
  431.                 'description' => 'Enable or disable pagination',
  432.             ];
  433.             $v3 ? $paginationEnabledParameter['schema'] = ['type' => 'boolean'] : $paginationEnabledParameter['type'] = 'boolean';
  434.             $pathOperation['parameters'][] = $paginationEnabledParameter;
  435.         }
  436.     }
  437.     /**
  438.      * @throws ResourceClassNotFoundException
  439.      */
  440.     private function addSubresourceOperation(bool $v3, array $subresourceOperation, \ArrayObject $definitions, string $operationId, ResourceMetadata $resourceMetadata): \ArrayObject
  441.     {
  442.         $operationName = 'get'; // TODO: we might want to extract that at some point to also support other subresource operations
  443.         $collection = $subresourceOperation['collection'] ?? false;
  444.         $subResourceMetadata = $this->resourceMetadataFactory->create($subresourceOperation['resource_class']);
  445.         $pathOperation = new \ArrayObject([]);
  446.         $pathOperation['tags'] = $subresourceOperation['shortNames'];
  447.         $pathOperation['operationId'] = $operationId;
  448.         $pathOperation['summary'] = sprintf('Retrieves %s%s resource%s.', $subresourceOperation['collection'] ? 'the collection of ' : 'a ', $subresourceOperation['shortNames'][0], $subresourceOperation['collection'] ? 's' : '');
  449.         if (null === $this->formatsProvider) {
  450.             // TODO: Subresource operation metadata aren't available by default, for now we have to fallback on default formats.
  451.             // TODO: A better approach would be to always populate the subresource operation array.
  452.             $subResourceMetadata = $this
  453.                 ->resourceMetadataFactory
  454.                 ->create($subresourceOperation['resource_class']);
  455.             if ($this->resourceMetadataFactory instanceof ResourceMetadataCollectionFactoryInterface) {
  456.                 $subResourceMetadata = $this->transformResourceToResourceMetadata($subResourceMetadata[0]);
  457.             }
  458.             $responseFormats = $subResourceMetadata->getTypedOperationAttribute(OperationType::SUBRESOURCE, $operationName, 'output_formats', $this->formats, true);
  459.         } else {
  460.             $responseFormats = $this->formatsProvider->getFormatsFromOperation($subresourceOperation['resource_class'], $operationName, OperationType::SUBRESOURCE);
  461.         }
  462.         $mimeTypes = $this->flattenMimeTypes($responseFormats);
  463.         if (!$v3) {
  464.             $pathOperation['produces'] = array_keys($mimeTypes);
  465.         }
  466.         $successResponse = [
  467.             'description' => sprintf('%s %s response', $subresourceOperation['shortNames'][0], $collection ? 'collection' : 'resource'),
  468.         ];
  469.         [$successResponse] = $this->addSchemas($v3, $successResponse, $definitions, $subresourceOperation['resource_class'], OperationType::SUBRESOURCE, $operationName, $mimeTypes, Schema::TYPE_OUTPUT, $collection);
  470.         $pathOperation['responses'] = ['200' => $successResponse, '404' => ['description' => 'Resource not found']];
  471.         // Avoid duplicates parameters when there is a filter on a subresource identifier
  472.         $parametersMemory = [];
  473.         $pathOperation['parameters'] = [];
  474.         foreach ($subresourceOperation['identifiers'] as $parameterName => [$class, $identifier, $hasIdentifier]) {
  475.             if (!str_contains($subresourceOperation['path'], sprintf('{%s}', $parameterName))) {
  476.                 continue;
  477.             }
  478.             $parameter = ['name' => $parameterName, 'in' => 'path', 'required' => true];
  479.             $v3 ? $parameter['schema'] = ['type' => 'string'] : $parameter['type'] = 'string';
  480.             $pathOperation['parameters'][] = $parameter;
  481.             $parametersMemory[] = $parameterName;
  482.         }
  483.         if ($parameters = $this->getFiltersParameters($v3, $subresourceOperation['resource_class'], $operationName, $subResourceMetadata)) {
  484.             foreach ($parameters as $parameter) {
  485.                 if (!\in_array($parameter['name'], $parametersMemory, true)) {
  486.                     $pathOperation['parameters'][] = $parameter;
  487.                 }
  488.             }
  489.         }
  490.         if ($subresourceOperation['collection']) {
  491.             $this->addPaginationParameters($v3, $subResourceMetadata, OperationType::SUBRESOURCE, $subresourceOperation['operation_name'], $pathOperation);
  492.         }
  493.         return $pathOperation;
  494.     }
  495.     private function updatePostOperation(bool $v3, \ArrayObject $pathOperation, array $requestMimeTypes, array $responseMimeTypes, string $operationType, ResourceMetadata $resourceMetadata, string $resourceClass, string $resourceShortName, string $operationName, \ArrayObject $definitions, \ArrayObject $links): \ArrayObject
  496.     {
  497.         if (!$v3) {
  498.             $pathOperation['consumes'] ?? $pathOperation['consumes'] = array_keys($requestMimeTypes);
  499.             $pathOperation['produces'] ?? $pathOperation['produces'] = array_keys($responseMimeTypes);
  500.         }
  501.         $pathOperation['summary'] ?? $pathOperation['summary'] = sprintf('Creates a %s resource.', $resourceShortName);
  502.         $identifiers = (array) $resourceMetadata
  503.                 ->getTypedOperationAttribute($operationType, $operationName, 'identifiers', [], false);
  504.         $pathOperation = $this->addItemOperationParameters($v3, $pathOperation, $operationType, $operationName, $resourceMetadata, $resourceClass, OperationType::ITEM === $operationType ? false : true);
  505.         $successResponse = ['description' => sprintf('%s resource created', $resourceShortName)];
  506.         [$successResponse, $defined] = $this->addSchemas($v3, $successResponse, $definitions, $resourceClass, $operationType, $operationName, $responseMimeTypes);
  507.         if ($defined && $v3 && ($links[$key = 'get'.ucfirst($resourceShortName).ucfirst(OperationType::ITEM)] ?? null)) {
  508.             $successResponse['links'] = [ucfirst($key) => $links[$key]];
  509.         }
  510.         $pathOperation['responses'] ?? $pathOperation['responses'] = [
  511.             (string) $resourceMetadata->getTypedOperationAttribute($operationType, $operationName, 'status', '201') => $successResponse,
  512.             '400' => ['description' => 'Invalid input'],
  513.             '404' => ['description' => 'Resource not found'],
  514.             '422' => ['description' => 'Unprocessable entity'],
  515.         ];
  516.         return $this->addRequestBody($v3, $pathOperation, $definitions, $resourceClass, $resourceShortName, $operationType, $operationName, $requestMimeTypes);
  517.     }
  518.     private function updatePutOperation(bool $v3, \ArrayObject $pathOperation, array $requestMimeTypes, array $responseMimeTypes, string $operationType, ResourceMetadata $resourceMetadata, string $resourceClass, string $resourceShortName, string $operationName, \ArrayObject $definitions): \ArrayObject
  519.     {
  520.         if (!$v3) {
  521.             $pathOperation['consumes'] ?? $pathOperation['consumes'] = array_keys($requestMimeTypes);
  522.             $pathOperation['produces'] ?? $pathOperation['produces'] = array_keys($responseMimeTypes);
  523.         }
  524.         $pathOperation['summary'] ?? $pathOperation['summary'] = sprintf('Replaces the %s resource.', $resourceShortName);
  525.         $pathOperation = $this->addItemOperationParameters($v3, $pathOperation, $operationType, $operationName, $resourceMetadata, $resourceClass);
  526.         $successResponse = ['description' => sprintf('%s resource updated', $resourceShortName)];
  527.         [$successResponse] = $this->addSchemas($v3, $successResponse, $definitions, $resourceClass, $operationType, $operationName, $responseMimeTypes);
  528.         $pathOperation['responses'] ?? $pathOperation['responses'] = [
  529.             (string) $resourceMetadata->getTypedOperationAttribute($operationType, $operationName, 'status', '200') => $successResponse,
  530.             '400' => ['description' => 'Invalid input'],
  531.             '404' => ['description' => 'Resource not found'],
  532.             '422' => ['description' => 'Unprocessable entity'],
  533.         ];
  534.         return $this->addRequestBody($v3, $pathOperation, $definitions, $resourceClass, $resourceShortName, $operationType, $operationName, $requestMimeTypes, true);
  535.     }
  536.     private function addRequestBody(bool $v3, \ArrayObject $pathOperation, \ArrayObject $definitions, string $resourceClass, string $resourceShortName, string $operationType, string $operationName, array $requestMimeTypes, bool $put = false)
  537.     {
  538.         if (isset($pathOperation['requestBody'])) {
  539.             return $pathOperation;
  540.         }
  541.         [$message, $defined] = $this->addSchemas($v3, [], $definitions, $resourceClass, $operationType, $operationName, $requestMimeTypes, Schema::TYPE_INPUT);
  542.         if (!$defined) {
  543.             return $pathOperation;
  544.         }
  545.         $description = sprintf('The %s %s resource', $put ? 'updated' : 'new', $resourceShortName);
  546.         if ($v3) {
  547.             $pathOperation['requestBody'] = $message + ['description' => $description];
  548.             return $pathOperation;
  549.         }
  550.         if (!$this->hasBodyParameter($pathOperation['parameters'] ?? [])) {
  551.             $pathOperation['parameters'][] = [
  552.                 'name' => lcfirst($resourceShortName),
  553.                 'in' => 'body',
  554.                 'description' => $description,
  555.             ] + $message;
  556.         }
  557.         return $pathOperation;
  558.     }
  559.     private function hasBodyParameter(array $parameters): bool
  560.     {
  561.         foreach ($parameters as $parameter) {
  562.             if (\array_key_exists('in', $parameter) && 'body' === $parameter['in']) {
  563.                 return true;
  564.             }
  565.         }
  566.         return false;
  567.     }
  568.     private function updateDeleteOperation(bool $v3, \ArrayObject $pathOperation, string $resourceShortName, string $operationType, string $operationName, ResourceMetadata $resourceMetadata, string $resourceClass): \ArrayObject
  569.     {
  570.         $pathOperation['summary'] ?? $pathOperation['summary'] = sprintf('Removes the %s resource.', $resourceShortName);
  571.         $pathOperation['responses'] ?? $pathOperation['responses'] = [
  572.             (string) $resourceMetadata->getTypedOperationAttribute($operationType, $operationName, 'status', '204') => ['description' => sprintf('%s resource deleted', $resourceShortName)],
  573.             '404' => ['description' => 'Resource not found'],
  574.         ];
  575.         return $this->addItemOperationParameters($v3, $pathOperation, $operationType, $operationName, $resourceMetadata, $resourceClass);
  576.     }
  577.     private function addItemOperationParameters(bool $v3, \ArrayObject $pathOperation, string $operationType, string $operationName, ResourceMetadata $resourceMetadata, string $resourceClass, bool $isPost = false): \ArrayObject
  578.     {
  579.         $identifiers = (array) $resourceMetadata
  580.                 ->getTypedOperationAttribute($operationType, $operationName, 'identifiers', [], false);
  581.         // Auto-generated routes in API Platform < 2.7 are considered as collection, hotfix this as the OpenApi Factory supports new operations anyways.
  582.         // this also fixes a bug where we could not create POST item operations in API P 2.6
  583.         if (OperationType::ITEM === $operationType && $isPost) {
  584.             $operationType = OperationType::COLLECTION;
  585.         }
  586.         if (!$identifiers && OperationType::COLLECTION !== $operationType) {
  587.             try {
  588.                 $identifiers = $this->identifiersExtractor->getIdentifiersFromResourceClass($resourceClass);
  589.             } catch (RuntimeException $e) {
  590.                 // Ignore exception here
  591.             } catch (ResourceClassNotFoundException $e) {
  592.                 if (false === $this->legacyMode) {
  593.                     // Skipping these, swagger is not compatible with post 2.7 resource metadata
  594.                     return $pathOperation;
  595.                 }
  596.                 throw $e;
  597.             }
  598.         }
  599.         if (\count($identifiers) > 1 ? $resourceMetadata->getItemOperationAttribute($operationName, 'composite_identifier', true, true) : false) {
  600.             $identifiers = ['id'];
  601.         }
  602.         if (!$identifiers && OperationType::COLLECTION === $operationType) {
  603.             return $pathOperation;
  604.         }
  605.         if (!isset($pathOperation['parameters'])) {
  606.             $pathOperation['parameters'] = [];
  607.         }
  608.         foreach ($identifiers as $parameterName => $identifier) {
  609.             $parameter = [
  610.                 'name' => \is_string($parameterName) ? $parameterName : $identifier,
  611.                 'in' => 'path',
  612.                 'required' => true,
  613.             ];
  614.             $v3 ? $parameter['schema'] = ['type' => 'string'] : $parameter['type'] = 'string';
  615.             $pathOperation['parameters'][] = $parameter;
  616.         }
  617.         return $pathOperation;
  618.     }
  619.     private function getJsonSchema(bool $v3, \ArrayObject $definitions, string $resourceClass, string $type, ?string $operationType, ?string $operationName, string $format = 'json', array $serializerContext = null, bool $forceCollection = false): Schema
  620.     {
  621.         $schema = new Schema($v3 ? Schema::VERSION_OPENAPI : Schema::VERSION_SWAGGER);
  622.         $schema->setDefinitions($definitions);
  623.         if ($this->jsonSchemaFactory instanceof SchemaFactoryInterface) {
  624.             $operation = $operationName ? (new class() extends HttpOperation {})->withName($operationName) : null;
  625.             return $this->jsonSchemaFactory->buildSchema($resourceClass, $format, $type, $operation, $schema, $serializerContext, $forceCollection);
  626.         }
  627.         return $this->jsonSchemaFactory->buildSchema($resourceClass, $format, $type, $operationType, $operationName, $schema, $serializerContext, $forceCollection);
  628.     }
  629.     private function computeDoc(bool $v3, Documentation $documentation, \ArrayObject $definitions, \ArrayObject $paths, array $context): array
  630.     {
  631.         $baseUrl = $context[self::BASE_URL] ?? $this->defaultContext[self::BASE_URL];
  632.         if ($v3) {
  633.             $docs = ['openapi' => self::OPENAPI_VERSION];
  634.             if ('/' !== $baseUrl && '' !== $baseUrl) {
  635.                 $docs['servers'] = [['url' => $baseUrl]];
  636.             }
  637.         } else {
  638.             $docs = [
  639.                 'swagger' => self::SWAGGER_VERSION,
  640.                 'basePath' => $baseUrl,
  641.             ];
  642.         }
  643.         $docs += [
  644.             'info' => [
  645.                 'title' => $documentation->getTitle(),
  646.                 'version' => $documentation->getVersion(),
  647.             ],
  648.             'paths' => $paths,
  649.         ];
  650.         if ('' !== $description = $documentation->getDescription()) {
  651.             $docs['info']['description'] = $description;
  652.         }
  653.         $securityDefinitions = [];
  654.         $security = [];
  655.         if ($this->oauthEnabled) {
  656.             $oauthAttributes = [
  657.                 'authorizationUrl' => $this->oauthAuthorizationUrl,
  658.                 'scopes' => new \ArrayObject($this->oauthScopes),
  659.             ];
  660.             if ($this->oauthTokenUrl) {
  661.                 $oauthAttributes['tokenUrl'] = $this->oauthTokenUrl;
  662.             }
  663.             $securityDefinitions['oauth'] = [
  664.                 'type' => $this->oauthType,
  665.                 'description' => sprintf(
  666.                     'OAuth 2.0 %s Grant',
  667.                     strtolower(preg_replace('/[A-Z]/', ' \\0', lcfirst($this->oauthFlow)))
  668.                 ),
  669.             ];
  670.             if ($v3) {
  671.                 $securityDefinitions['oauth']['flows'] = [
  672.                     $this->oauthFlow => $oauthAttributes,
  673.                 ];
  674.             } else {
  675.                 $securityDefinitions['oauth']['flow'] = $this->oauthFlow;
  676.                 $securityDefinitions['oauth'] = array_merge($securityDefinitions['oauth'], $oauthAttributes);
  677.             }
  678.             $security[] = ['oauth' => []];
  679.         }
  680.         foreach ($this->apiKeys as $key => $apiKey) {
  681.             $name = $apiKey['name'];
  682.             $type = $apiKey['type'];
  683.             $securityDefinitions[$key] = [
  684.                 'type' => 'apiKey',
  685.                 'in' => $type,
  686.                 'description' => sprintf('Value for the %s %s', $name, 'query' === $type ? sprintf('%s parameter', $type) : $type),
  687.                 'name' => $name,
  688.             ];
  689.             $security[] = [$key => []];
  690.         }
  691.         if ($securityDefinitions && $security) { // @phpstan-ignore-line false positive
  692.             $docs['security'] = $security;
  693.             if (!$v3) {
  694.                 $docs['securityDefinitions'] = $securityDefinitions;
  695.             }
  696.         }
  697.         if ($v3) {
  698.             if (\count($definitions) + \count($securityDefinitions)) {
  699.                 $docs['components'] = [];
  700.                 if (\count($definitions)) {
  701.                     $docs['components']['schemas'] = $definitions;
  702.                 }
  703.                 if (\count($securityDefinitions)) {
  704.                     $docs['components']['securitySchemes'] = $securityDefinitions;
  705.                 }
  706.             }
  707.         } elseif (\count($definitions) > 0) {
  708.             $docs['definitions'] = $definitions;
  709.         }
  710.         return $docs;
  711.     }
  712.     /**
  713.      * Gets parameters corresponding to enabled filters.
  714.      */
  715.     private function getFiltersParameters(bool $v3, string $resourceClass, string $operationName, ResourceMetadata $resourceMetadata): array
  716.     {
  717.         if (null === $this->filterLocator) {
  718.             return [];
  719.         }
  720.         $parameters = [];
  721.         $resourceFilters = $resourceMetadata->getCollectionOperationAttribute($operationName, 'filters', [], true);
  722.         foreach ($resourceFilters as $filterId) {
  723.             if (!$filter = $this->getFilter($filterId)) {
  724.                 continue;
  725.             }
  726.             foreach ($filter->getDescription($resourceClass) as $name => $data) {
  727.                 $parameter = [
  728.                     'name' => $name,
  729.                     'in' => 'query',
  730.                     'required' => $data['required'],
  731.                 ];
  732.                 $type = \in_array($data['type'], Type::$builtinTypes, true) ? $this->jsonSchemaTypeFactory->getType(new Type($data['type'], false, null, $data['is_collection'] ?? false)) : ['type' => 'string'];
  733.                 $v3 ? $parameter['schema'] = $type : $parameter += $type;
  734.                 if ($v3 && isset($data['schema'])) {
  735.                     $parameter['schema'] = $data['schema'];
  736.                 }
  737.                 if ('array' === ($type['type'] ?? '')) {
  738.                     $deepObject = \in_array($data['type'], [Type::BUILTIN_TYPE_ARRAY, Type::BUILTIN_TYPE_OBJECT], true);
  739.                     if ($v3) {
  740.                         $parameter['style'] = $deepObject ? 'deepObject' : 'form';
  741.                         $parameter['explode'] = true;
  742.                     } else {
  743.                         $parameter['collectionFormat'] = $deepObject ? 'csv' : 'multi';
  744.                     }
  745.                 }
  746.                 $key = $v3 ? 'openapi' : 'swagger';
  747.                 if (isset($data[$key])) {
  748.                     $parameter = $data[$key] + $parameter;
  749.                 }
  750.                 $parameters[] = $parameter;
  751.             }
  752.         }
  753.         return $parameters;
  754.     }
  755.     public function supportsNormalization($data, $format = null, array $context = []): bool
  756.     {
  757.         return self::FORMAT === $format && ($data instanceof Documentation || $this->openApiNormalizer && $data instanceof OpenApi);
  758.     }
  759.     public function hasCacheableSupportsMethod(): bool
  760.     {
  761.         return true;
  762.     }
  763.     private function flattenMimeTypes(array $responseFormats): array
  764.     {
  765.         $responseMimeTypes = [];
  766.         foreach ($responseFormats as $responseFormat => $mimeTypes) {
  767.             foreach ($mimeTypes as $mimeType) {
  768.                 $responseMimeTypes[$mimeType] = $responseFormat;
  769.             }
  770.         }
  771.         return $responseMimeTypes;
  772.     }
  773.     /**
  774.      * https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.0.md#linkObject.
  775.      */
  776.     private function getLinkObject(string $resourceClass, string $operationId, string $path): array
  777.     {
  778.         $linkObject = $identifiers = [];
  779.         foreach ($this->propertyNameCollectionFactory->create($resourceClass) as $propertyName) {
  780.             $propertyMetadata = $this->propertyMetadataFactory->create($resourceClass, $propertyName);
  781.             if (!$propertyMetadata->isIdentifier()) {
  782.                 continue;
  783.             }
  784.             $linkObject['parameters'][$propertyName] = sprintf('$response.body#/%s', $propertyName);
  785.             $identifiers[] = $propertyName;
  786.         }
  787.         if (!$linkObject) {
  788.             return [];
  789.         }
  790.         $linkObject['operationId'] = $operationId;
  791.         $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);
  792.         return $linkObject;
  793.     }
  794. }