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

  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 NormalizerInterfaceCacheableSupportsMethodInterface
  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.      * @param mixed                                                                                   $resourceMetadataFactory
  112.      */
  113.     public function __construct($resourceMetadataFactoryPropertyNameCollectionFactoryInterface $propertyNameCollectionFactoryPropertyMetadataFactoryInterface $propertyMetadataFactory$jsonSchemaFactory null$jsonSchemaTypeFactory nullOperationPathResolverInterface $operationPathResolver nullUrlGeneratorInterface $urlGenerator null$filterLocator nullNameConverterInterface $nameConverter nullbool $oauthEnabled falsestring $oauthType ''string $oauthFlow ''string $oauthTokenUrl ''string $oauthAuthorizationUrl '', array $oauthScopes = [], array $apiKeys = [], SubresourceOperationFactoryInterface $subresourceOperationFactory nullbool $paginationEnabled truestring $paginationPageParameterName 'page'bool $clientItemsPerPage falsestring $itemsPerPageParameterName 'itemsPerPage'$formats = [], bool $paginationClientEnabled falsestring $paginationClientEnabledParameterName 'pagination', array $defaultContext = [], array $swaggerVersions = [23], IdentifiersExtractorInterface $identifiersExtractor nullNormalizerInterface $openApiNormalizer nullbool $legacyMode false)
  114.     {
  115.         if ($jsonSchemaTypeFactory instanceof OperationMethodResolverInterface) {
  116.             @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);
  117.             $this->operationMethodResolver $jsonSchemaTypeFactory;
  118.             $this->jsonSchemaTypeFactory = new TypeFactory();
  119.         } else {
  120.             $this->jsonSchemaTypeFactory $jsonSchemaTypeFactory ?? new TypeFactory();
  121.         }
  122.         if ($jsonSchemaFactory instanceof ResourceClassResolverInterface) {
  123.             @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);
  124.         }
  125.         if (null === $jsonSchemaFactory || $jsonSchemaFactory instanceof ResourceClassResolverInterface) {
  126.             if ($resourceMetadataFactory instanceof ResourceMetadataFactoryInterface) {
  127.                 $jsonSchemaFactory = new LegacySchemaFactory($this->jsonSchemaTypeFactory$resourceMetadataFactory$propertyNameCollectionFactory$propertyMetadataFactory$nameConverter);
  128.             } else {
  129.                 $jsonSchemaFactory = new SchemaFactory($this->jsonSchemaTypeFactory$resourceMetadataFactory$propertyNameCollectionFactory$propertyMetadataFactory$nameConverter);
  130.             }
  131.             $this->jsonSchemaTypeFactory->setSchemaFactory($jsonSchemaFactory);
  132.         }
  133.         $this->jsonSchemaFactory $jsonSchemaFactory;
  134.         if ($nameConverter) {
  135.             @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);
  136.         }
  137.         if ($urlGenerator) {
  138.             @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);
  139.         }
  140.         if ($formats instanceof FormatsProviderInterface) {
  141.             @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);
  142.             $this->formatsProvider $formats;
  143.         } else {
  144.             $this->formats $formats;
  145.         }
  146.         $this->setFilterLocator($filterLocatortrue);
  147.         if ($resourceMetadataFactory instanceof ResourceMetadataFactoryInterface) {
  148.             trigger_deprecation('api-platform/core''2.7'sprintf('Use "%s" instead of "%s".'ResourceMetadataCollectionFactoryInterface::class, ResourceMetadataFactoryInterface::class));
  149.         }
  150.         $this->resourceMetadataFactory $resourceMetadataFactory;
  151.         $this->propertyNameCollectionFactory $propertyNameCollectionFactory;
  152.         $this->propertyMetadataFactory $propertyMetadataFactory;
  153.         $this->operationPathResolver $operationPathResolver;
  154.         $this->oauthEnabled $oauthEnabled;
  155.         $this->oauthType $oauthType;
  156.         $this->oauthFlow $oauthFlow;
  157.         $this->oauthTokenUrl $oauthTokenUrl;
  158.         $this->oauthAuthorizationUrl $oauthAuthorizationUrl;
  159.         $this->oauthScopes $oauthScopes;
  160.         $this->subresourceOperationFactory $subresourceOperationFactory;
  161.         $this->paginationEnabled $paginationEnabled;
  162.         $this->paginationPageParameterName $paginationPageParameterName;
  163.         $this->apiKeys $apiKeys;
  164.         $this->clientItemsPerPage $clientItemsPerPage;
  165.         $this->itemsPerPageParameterName $itemsPerPageParameterName;
  166.         $this->paginationClientEnabled $paginationClientEnabled;
  167.         $this->paginationClientEnabledParameterName $paginationClientEnabledParameterName;
  168.         $this->defaultContext[self::SPEC_VERSION] = $swaggerVersions[0] ?? 2;
  169.         $this->defaultContext array_merge($this->defaultContext$defaultContext);
  170.         $this->identifiersExtractor $identifiersExtractor;
  171.         $this->openApiNormalizer $openApiNormalizer;
  172.         $this->legacyMode $legacyMode;
  173.     }
  174.     /**
  175.      * {@inheritdoc}
  176.      *
  177.      * @return array|string|int|float|bool|\ArrayObject|null
  178.      */
  179.     public function normalize($object$format null, array $context = [])
  180.     {
  181.         if ($object instanceof OpenApi) {
  182.             @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);
  183.             return $this->openApiNormalizer->normalize($object$format$context);
  184.         }
  185.         $v3 === ($context['spec_version'] ?? $this->defaultContext['spec_version']) && !($context['api_gateway'] ?? $this->defaultContext['api_gateway']);
  186.         $definitions = new \ArrayObject();
  187.         $paths = new \ArrayObject();
  188.         $links = new \ArrayObject();
  189.         if ($this->resourceMetadataFactory instanceof ResourceMetadataCollectionFactoryInterface) {
  190.             foreach ($object->getResourceNameCollection() as $resourceClass) {
  191.                 $resourceMetadataCollection $this->resourceMetadataFactory->create($resourceClass);
  192.                 foreach ($resourceMetadataCollection as $i => $resourceMetadata) {
  193.                     $resourceMetadata $this->transformResourceToResourceMetadata($resourceMetadata);
  194.                     // Items needs to be parsed first to be able to reference the lines from the collection operation
  195.                     $this->addPaths($v3$paths$definitions$resourceClass$resourceMetadata->getShortName(), $resourceMetadataOperationType::ITEM$links);
  196.                     $this->addPaths($v3$paths$definitions$resourceClass$resourceMetadata->getShortName(), $resourceMetadataOperationType::COLLECTION$links);
  197.                 }
  198.             }
  199.             $definitions->ksort();
  200.             $paths->ksort();
  201.             return $this->computeDoc($v3$object$definitions$paths$context);
  202.         }
  203.         foreach ($object->getResourceNameCollection() as $resourceClass) {
  204.             $resourceMetadata $this->resourceMetadataFactory->create($resourceClass);
  205.             if ($this->identifiersExtractor) {
  206.                 $identifiers = [];
  207.                 if ($resourceMetadata->getItemOperations()) {
  208.                     $identifiers $this->identifiersExtractor->getIdentifiersFromResourceClass($resourceClass);
  209.                 }
  210.                 $resourceMetadata $resourceMetadata->withAttributes(($resourceMetadata->getAttributes() ?: []) + ['identifiers' => $identifiers]);
  211.             }
  212.             $resourceShortName $resourceMetadata->getShortName();
  213.             // Items needs to be parsed first to be able to reference the lines from the collection operation
  214.             $this->addPaths($v3$paths$definitions$resourceClass$resourceShortName$resourceMetadataOperationType::ITEM$links);
  215.             $this->addPaths($v3$paths$definitions$resourceClass$resourceShortName$resourceMetadataOperationType::COLLECTION$links);
  216.             if (null === $this->subresourceOperationFactory) {
  217.                 continue;
  218.             }
  219.             foreach ($this->subresourceOperationFactory->create($resourceClass) as $operationId => $subresourceOperation) {
  220.                 $method $resourceMetadata->getTypedOperationAttribute(OperationType::SUBRESOURCE$subresourceOperation['operation_name'], 'method''GET');
  221.                 $paths[$this->getPath($subresourceOperation['shortNames'][0], $subresourceOperation['route_name'], $subresourceOperationOperationType::SUBRESOURCE)][strtolower($method)] = $this->addSubresourceOperation($v3$subresourceOperation$definitions$operationId$resourceMetadata);
  222.             }
  223.         }
  224.         $definitions->ksort();
  225.         $paths->ksort();
  226.         return $this->computeDoc($v3$object$definitions$paths$context);
  227.     }
  228.     /**
  229.      * Updates the list of entries in the paths collection.
  230.      */
  231.     private function addPaths(bool $v3\ArrayObject $paths\ArrayObject $definitionsstring $resourceClassstring $resourceShortNameResourceMetadata $resourceMetadatastring $operationType\ArrayObject $links)
  232.     {
  233.         if (null === $operations OperationType::COLLECTION === $operationType $resourceMetadata->getCollectionOperations() : $resourceMetadata->getItemOperations()) {
  234.             return;
  235.         }
  236.         foreach ($operations as $operationName => $operation) {
  237.             if (false === ($operation['openapi'] ?? null)) {
  238.                 continue;
  239.             }
  240.             // Skolem IRI
  241.             if ('api_genid' === ($operation['route_name'] ?? null)) {
  242.                 continue;
  243.             }
  244.             if (isset($operation['uri_template'])) {
  245.                 $path str_replace('.{_format}'''$operation['uri_template']);
  246.                 if (!== strpos($path'/')) {
  247.                     $path '/'.$path;
  248.                 }
  249.             } else {
  250.                 $path $this->getPath($resourceShortName$operationName$operation$operationType);
  251.             }
  252.             if ($this->operationMethodResolver) {
  253.                 $method OperationType::ITEM === $operationType $this->operationMethodResolver->getItemOperationMethod($resourceClass$operationName) : $this->operationMethodResolver->getCollectionOperationMethod($resourceClass$operationName);
  254.             } else {
  255.                 $method $resourceMetadata->getTypedOperationAttribute($operationType$operationName'method''GET');
  256.             }
  257.             $paths[$path][strtolower($method)] = $this->getPathOperation($v3$operationName$operation$method$operationType$resourceClass$resourceMetadata$definitions$links);
  258.         }
  259.     }
  260.     /**
  261.      * Gets the path for an operation.
  262.      *
  263.      * If the path ends with the optional _format parameter, it is removed
  264.      * as optional path parameters are not yet supported.
  265.      *
  266.      * @see https://github.com/OAI/OpenAPI-Specification/issues/93
  267.      */
  268.     private function getPath(string $resourceShortNamestring $operationName, array $operationstring $operationType): string
  269.     {
  270.         $path $this->operationPathResolver->resolveOperationPath($resourceShortName$operation$operationType$operationName);
  271.         if ('.{_format}' === substr($path, -10)) {
  272.             $path substr($path0, -10);
  273.         }
  274.         return $path;
  275.     }
  276.     /**
  277.      * Gets a path Operation Object.
  278.      *
  279.      * @see https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#operation-object
  280.      */
  281.     private function getPathOperation(bool $v3string $operationName, array $operationstring $methodstring $operationTypestring $resourceClassResourceMetadata $resourceMetadata\ArrayObject $definitions\ArrayObject $links): \ArrayObject
  282.     {
  283.         $pathOperation = new \ArrayObject($operation[$v3 'openapi_context' 'swagger_context'] ?? []);
  284.         $resourceShortName $resourceMetadata->getShortName();
  285.         $pathOperation['tags'] ?? $pathOperation['tags'] = [$resourceShortName];
  286.         $pathOperation['operationId'] ?? $pathOperation['operationId'] = lcfirst($operationName).ucfirst($resourceShortName).ucfirst($operationType);
  287.         if ($v3 && 'GET' === $method && OperationType::ITEM === $operationType && $link $this->getLinkObject($resourceClass$pathOperation['operationId'], $this->getPath($resourceShortName$operationName$operation$operationType))) {
  288.             $links[$pathOperation['operationId']] = $link;
  289.         }
  290.         if ($resourceMetadata->getTypedOperationAttribute($operationType$operationName'deprecation_reason'nulltrue)) {
  291.             $pathOperation['deprecated'] = true;
  292.         }
  293.         if (null === $this->formatsProvider) {
  294.             $requestFormats $resourceMetadata->getTypedOperationAttribute($operationType$operationName'input_formats', [], true);
  295.             $responseFormats $resourceMetadata->getTypedOperationAttribute($operationType$operationName'output_formats', [], true);
  296.         } else {
  297.             $requestFormats $responseFormats $this->formatsProvider->getFormatsFromOperation($resourceClass$operationName$operationType);
  298.         }
  299.         $requestMimeTypes $this->flattenMimeTypes($requestFormats);
  300.         $responseMimeTypes $this->flattenMimeTypes($responseFormats);
  301.         switch ($method) {
  302.             case 'GET':
  303.                 return $this->updateGetOperation($v3$pathOperation$responseMimeTypes$operationType$resourceMetadata$resourceClass$resourceShortName$operationName$definitions);
  304.             case 'POST':
  305.                 return $this->updatePostOperation($v3$pathOperation$requestMimeTypes$responseMimeTypes$operationType$resourceMetadata$resourceClass$resourceShortName$operationName$definitions$links);
  306.             case 'PATCH':
  307.                 $pathOperation['summary'] ?? $pathOperation['summary'] = sprintf('Updates the %s resource.'$resourceShortName);
  308.                 // no break
  309.             case 'PUT':
  310.                 return $this->updatePutOperation($v3$pathOperation$requestMimeTypes$responseMimeTypes$operationType$resourceMetadata$resourceClass$resourceShortName$operationName$definitions);
  311.             case 'DELETE':
  312.                 return $this->updateDeleteOperation($v3$pathOperation$resourceShortName$operationType$operationName$resourceMetadata$resourceClass);
  313.         }
  314.         return $pathOperation;
  315.     }
  316.     /**
  317.      * @return array the update message as first value, and if the schema is defined as second
  318.      */
  319.     private function addSchemas(bool $v3, array $message\ArrayObject $definitionsstring $resourceClassstring $operationTypestring $operationName, array $mimeTypesstring $type Schema::TYPE_OUTPUTbool $forceCollection false): array
  320.     {
  321.         if (!$v3) {
  322.             $jsonSchema $this->getJsonSchema($v3$definitions$resourceClass$type$operationType$operationName'json'null$forceCollection);
  323.             if (!$jsonSchema->isDefined()) {
  324.                 return [$messagefalse];
  325.             }
  326.             $message['schema'] = $jsonSchema->getArrayCopy(false);
  327.             return [$messagetrue];
  328.         }
  329.         foreach ($mimeTypes as $mimeType => $format) {
  330.             $jsonSchema $this->getJsonSchema($v3$definitions$resourceClass$type$operationType$operationName$formatnull$forceCollection);
  331.             if (!$jsonSchema->isDefined()) {
  332.                 return [$messagefalse];
  333.             }
  334.             $message['content'][$mimeType] = ['schema' => $jsonSchema->getArrayCopy(false)];
  335.         }
  336.         return [$messagetrue];
  337.     }
  338.     private function updateGetOperation(bool $v3\ArrayObject $pathOperation, array $mimeTypesstring $operationTypeResourceMetadata $resourceMetadatastring $resourceClassstring $resourceShortNamestring $operationName\ArrayObject $definitions): \ArrayObject
  339.     {
  340.         $successStatus = (string) $resourceMetadata->getTypedOperationAttribute($operationType$operationName'status''200');
  341.         if (!$v3) {
  342.             $pathOperation['produces'] ?? $pathOperation['produces'] = array_keys($mimeTypes);
  343.         }
  344.         if (OperationType::COLLECTION === $operationType) {
  345.             $outputResourseShortName $resourceMetadata->getCollectionOperations()[$operationName]['output']['name'] ?? $resourceShortName;
  346.             $pathOperation['summary'] ?? $pathOperation['summary'] = sprintf('Retrieves the collection of %s resources.'$outputResourseShortName);
  347.             $successResponse = ['description' => sprintf('%s collection response'$outputResourseShortName)];
  348.             [$successResponse] = $this->addSchemas($v3$successResponse$definitions$resourceClass$operationType$operationName$mimeTypes);
  349.             $pathOperation['responses'] ?? $pathOperation['responses'] = [$successStatus => $successResponse];
  350.             if (
  351.                 ($resourceMetadata->getAttributes()['extra_properties']['is_legacy_subresource'] ?? false) ||
  352.                 ($resourceMetadata->getAttributes()['extra_properties']['is_alternate_resource_metadata'] ?? false)) {
  353.                 // Avoid duplicates parameters when there is a filter on a subresource identifier
  354.                 $parametersMemory = [];
  355.                 $pathOperation['parameters'] = [];
  356.                 foreach ($resourceMetadata->getCollectionOperations()[$operationName]['identifiers'] as $parameterName => [$class$identifier]) {
  357.                     $parameter = ['name' => $parameterName'in' => 'path''required' => true];
  358.                     $v3 $parameter['schema'] = ['type' => 'string'] : $parameter['type'] = 'string';
  359.                     $pathOperation['parameters'][] = $parameter;
  360.                     $parametersMemory[] = $parameterName;
  361.                 }
  362.                 if ($parameters $this->getFiltersParameters($v3$resourceClass$operationName$resourceMetadata)) {
  363.                     foreach ($parameters as $parameter) {
  364.                         if (!\in_array($parameter['name'], $parametersMemorytrue)) {
  365.                             $pathOperation['parameters'][] = $parameter;
  366.                         }
  367.                     }
  368.                 }
  369.             } else {
  370.                 $pathOperation['parameters'] ?? $pathOperation['parameters'] = $this->getFiltersParameters($v3$resourceClass$operationName$resourceMetadata);
  371.             }
  372.             $this->addPaginationParameters($v3$resourceMetadataOperationType::COLLECTION$operationName$pathOperation);
  373.             return $pathOperation;
  374.         }
  375.         $outputResourseShortName $resourceMetadata->getItemOperations()[$operationName]['output']['name'] ?? $resourceShortName;
  376.         $pathOperation['summary'] ?? $pathOperation['summary'] = sprintf('Retrieves a %s resource.'$outputResourseShortName);
  377.         $pathOperation $this->addItemOperationParameters($v3$pathOperation$operationType$operationName$resourceMetadata$resourceClass);
  378.         $successResponse = ['description' => sprintf('%s resource response'$outputResourseShortName)];
  379.         [$successResponse] = $this->addSchemas($v3$successResponse$definitions$resourceClass$operationType$operationName$mimeTypes);
  380.         $pathOperation['responses'] ?? $pathOperation['responses'] = [
  381.             $successStatus => $successResponse,
  382.             '404' => ['description' => 'Resource not found'],
  383.         ];
  384.         return $pathOperation;
  385.     }
  386.     private function addPaginationParameters(bool $v3ResourceMetadata $resourceMetadatastring $operationTypestring $operationName\ArrayObject $pathOperation)
  387.     {
  388.         if ($this->paginationEnabled && $resourceMetadata->getTypedOperationAttribute($operationType$operationName'pagination_enabled'truetrue)) {
  389.             $paginationParameter = [
  390.                 'name' => $this->paginationPageParameterName,
  391.                 'in' => 'query',
  392.                 'required' => false,
  393.                 'description' => 'The collection page number',
  394.             ];
  395.             $v3 $paginationParameter['schema'] = [
  396.                 'type' => 'integer',
  397.                 'default' => 1,
  398.             ] : $paginationParameter['type'] = 'integer';
  399.             $pathOperation['parameters'][] = $paginationParameter;
  400.             if ($resourceMetadata->getTypedOperationAttribute($operationType$operationName'pagination_client_items_per_page'$this->clientItemsPerPagetrue)) {
  401.                 $itemPerPageParameter = [
  402.                     'name' => $this->itemsPerPageParameterName,
  403.                     'in' => 'query',
  404.                     'required' => false,
  405.                     'description' => 'The number of items per page',
  406.                 ];
  407.                 if ($v3) {
  408.                     $itemPerPageParameter['schema'] = [
  409.                         'type' => 'integer',
  410.                         'default' => $resourceMetadata->getTypedOperationAttribute($operationType$operationName'pagination_items_per_page'30true),
  411.                         'minimum' => 0,
  412.                     ];
  413.                     $maxItemsPerPage $resourceMetadata->getTypedOperationAttribute($operationType$operationName'maximum_items_per_page'nulltrue);
  414.                     if (null !== $maxItemsPerPage) {
  415.                         @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);
  416.                     }
  417.                     $maxItemsPerPage $resourceMetadata->getTypedOperationAttribute($operationType$operationName'pagination_maximum_items_per_page'$maxItemsPerPagetrue);
  418.                     if (null !== $maxItemsPerPage) {
  419.                         $itemPerPageParameter['schema']['maximum'] = $maxItemsPerPage;
  420.                     }
  421.                 } else {
  422.                     $itemPerPageParameter['type'] = 'integer';
  423.                 }
  424.                 $pathOperation['parameters'][] = $itemPerPageParameter;
  425.             }
  426.         }
  427.         if ($this->paginationEnabled && $resourceMetadata->getTypedOperationAttribute($operationType$operationName'pagination_client_enabled'$this->paginationClientEnabledtrue)) {
  428.             $paginationEnabledParameter = [
  429.                 'name' => $this->paginationClientEnabledParameterName,
  430.                 'in' => 'query',
  431.                 'required' => false,
  432.                 'description' => 'Enable or disable pagination',
  433.             ];
  434.             $v3 $paginationEnabledParameter['schema'] = ['type' => 'boolean'] : $paginationEnabledParameter['type'] = 'boolean';
  435.             $pathOperation['parameters'][] = $paginationEnabledParameter;
  436.         }
  437.     }
  438.     /**
  439.      * @throws ResourceClassNotFoundException
  440.      */
  441.     private function addSubresourceOperation(bool $v3, array $subresourceOperation\ArrayObject $definitionsstring $operationIdResourceMetadata $resourceMetadata): \ArrayObject
  442.     {
  443.         $operationName 'get'// TODO: we might want to extract that at some point to also support other subresource operations
  444.         $collection $subresourceOperation['collection'] ?? false;
  445.         $subResourceMetadata $this->resourceMetadataFactory->create($subresourceOperation['resource_class']);
  446.         $pathOperation = new \ArrayObject([]);
  447.         $pathOperation['tags'] = $subresourceOperation['shortNames'];
  448.         $pathOperation['operationId'] = $operationId;
  449.         $pathOperation['summary'] = sprintf('Retrieves %s%s resource%s.'$subresourceOperation['collection'] ? 'the collection of ' 'a '$subresourceOperation['shortNames'][0], $subresourceOperation['collection'] ? 's' '');
  450.         if (null === $this->formatsProvider) {
  451.             // TODO: Subresource operation metadata aren't available by default, for now we have to fallback on default formats.
  452.             // TODO: A better approach would be to always populate the subresource operation array.
  453.             $subResourceMetadata $this
  454.                 ->resourceMetadataFactory
  455.                 ->create($subresourceOperation['resource_class']);
  456.             if ($this->resourceMetadataFactory instanceof ResourceMetadataCollectionFactoryInterface) {
  457.                 $subResourceMetadata $this->transformResourceToResourceMetadata($subResourceMetadata[0]);
  458.             }
  459.             $responseFormats $subResourceMetadata->getTypedOperationAttribute(OperationType::SUBRESOURCE$operationName'output_formats'$this->formatstrue);
  460.         } else {
  461.             $responseFormats $this->formatsProvider->getFormatsFromOperation($subresourceOperation['resource_class'], $operationNameOperationType::SUBRESOURCE);
  462.         }
  463.         $mimeTypes $this->flattenMimeTypes($responseFormats);
  464.         if (!$v3) {
  465.             $pathOperation['produces'] = array_keys($mimeTypes);
  466.         }
  467.         $successResponse = [
  468.             'description' => sprintf('%s %s response'$subresourceOperation['shortNames'][0], $collection 'collection' 'resource'),
  469.         ];
  470.         [$successResponse] = $this->addSchemas($v3$successResponse$definitions$subresourceOperation['resource_class'], OperationType::SUBRESOURCE$operationName$mimeTypesSchema::TYPE_OUTPUT$collection);
  471.         $pathOperation['responses'] = ['200' => $successResponse'404' => ['description' => 'Resource not found']];
  472.         // Avoid duplicates parameters when there is a filter on a subresource identifier
  473.         $parametersMemory = [];
  474.         $pathOperation['parameters'] = [];
  475.         foreach ($subresourceOperation['identifiers'] as $parameterName => [$class$identifier$hasIdentifier]) {
  476.             if (false === strpos($subresourceOperation['path'], sprintf('{%s}'$parameterName))) {
  477.                 continue;
  478.             }
  479.             $parameter = ['name' => $parameterName'in' => 'path''required' => true];
  480.             $v3 $parameter['schema'] = ['type' => 'string'] : $parameter['type'] = 'string';
  481.             $pathOperation['parameters'][] = $parameter;
  482.             $parametersMemory[] = $parameterName;
  483.         }
  484.         if ($parameters $this->getFiltersParameters($v3$subresourceOperation['resource_class'], $operationName$subResourceMetadata)) {
  485.             foreach ($parameters as $parameter) {
  486.                 if (!\in_array($parameter['name'], $parametersMemorytrue)) {
  487.                     $pathOperation['parameters'][] = $parameter;
  488.                 }
  489.             }
  490.         }
  491.         if ($subresourceOperation['collection']) {
  492.             $this->addPaginationParameters($v3$subResourceMetadataOperationType::SUBRESOURCE$subresourceOperation['operation_name'], $pathOperation);
  493.         }
  494.         return $pathOperation;
  495.     }
  496.     private function updatePostOperation(bool $v3\ArrayObject $pathOperation, array $requestMimeTypes, array $responseMimeTypesstring $operationTypeResourceMetadata $resourceMetadatastring $resourceClassstring $resourceShortNamestring $operationName\ArrayObject $definitions\ArrayObject $links): \ArrayObject
  497.     {
  498.         if (!$v3) {
  499.             $pathOperation['consumes'] ?? $pathOperation['consumes'] = array_keys($requestMimeTypes);
  500.             $pathOperation['produces'] ?? $pathOperation['produces'] = array_keys($responseMimeTypes);
  501.         }
  502.         $pathOperation['summary'] ?? $pathOperation['summary'] = sprintf('Creates a %s resource.'$resourceShortName);
  503.         $identifiers = (array) $resourceMetadata
  504.                 ->getTypedOperationAttribute($operationType$operationName'identifiers', [], false);
  505.         $pathOperation $this->addItemOperationParameters($v3$pathOperation$operationType$operationName$resourceMetadata$resourceClassOperationType::ITEM === $operationType false true);
  506.         $successResponse = ['description' => sprintf('%s resource created'$resourceShortName)];
  507.         [$successResponse$defined] = $this->addSchemas($v3$successResponse$definitions$resourceClass$operationType$operationName$responseMimeTypes);
  508.         if ($defined && $v3 && ($links[$key 'get'.ucfirst($resourceShortName).ucfirst(OperationType::ITEM)] ?? null)) {
  509.             $successResponse['links'] = [ucfirst($key) => $links[$key]];
  510.         }
  511.         $pathOperation['responses'] ?? $pathOperation['responses'] = [
  512.             (string) $resourceMetadata->getTypedOperationAttribute($operationType$operationName'status''201') => $successResponse,
  513.             '400' => ['description' => 'Invalid input'],
  514.             '404' => ['description' => 'Resource not found'],
  515.             '422' => ['description' => 'Unprocessable entity'],
  516.         ];
  517.         return $this->addRequestBody($v3$pathOperation$definitions$resourceClass$resourceShortName$operationType$operationName$requestMimeTypes);
  518.     }
  519.     private function updatePutOperation(bool $v3\ArrayObject $pathOperation, array $requestMimeTypes, array $responseMimeTypesstring $operationTypeResourceMetadata $resourceMetadatastring $resourceClassstring $resourceShortNamestring $operationName\ArrayObject $definitions): \ArrayObject
  520.     {
  521.         if (!$v3) {
  522.             $pathOperation['consumes'] ?? $pathOperation['consumes'] = array_keys($requestMimeTypes);
  523.             $pathOperation['produces'] ?? $pathOperation['produces'] = array_keys($responseMimeTypes);
  524.         }
  525.         $pathOperation['summary'] ?? $pathOperation['summary'] = sprintf('Replaces the %s resource.'$resourceShortName);
  526.         $pathOperation $this->addItemOperationParameters($v3$pathOperation$operationType$operationName$resourceMetadata$resourceClass);
  527.         $successResponse = ['description' => sprintf('%s resource updated'$resourceShortName)];
  528.         [$successResponse] = $this->addSchemas($v3$successResponse$definitions$resourceClass$operationType$operationName$responseMimeTypes);
  529.         $pathOperation['responses'] ?? $pathOperation['responses'] = [
  530.             (string) $resourceMetadata->getTypedOperationAttribute($operationType$operationName'status''200') => $successResponse,
  531.             '400' => ['description' => 'Invalid input'],
  532.             '404' => ['description' => 'Resource not found'],
  533.             '422' => ['description' => 'Unprocessable entity'],
  534.         ];
  535.         return $this->addRequestBody($v3$pathOperation$definitions$resourceClass$resourceShortName$operationType$operationName$requestMimeTypestrue);
  536.     }
  537.     private function addRequestBody(bool $v3\ArrayObject $pathOperation\ArrayObject $definitionsstring $resourceClassstring $resourceShortNamestring $operationTypestring $operationName, array $requestMimeTypesbool $put false)
  538.     {
  539.         if (isset($pathOperation['requestBody'])) {
  540.             return $pathOperation;
  541.         }
  542.         [$message$defined] = $this->addSchemas($v3, [], $definitions$resourceClass$operationType$operationName$requestMimeTypesSchema::TYPE_INPUT);
  543.         if (!$defined) {
  544.             return $pathOperation;
  545.         }
  546.         $description sprintf('The %s %s resource'$put 'updated' 'new'$resourceShortName);
  547.         if ($v3) {
  548.             $pathOperation['requestBody'] = $message + ['description' => $description];
  549.             return $pathOperation;
  550.         }
  551.         if (!$this->hasBodyParameter($pathOperation['parameters'] ?? [])) {
  552.             $pathOperation['parameters'][] = [
  553.                 'name' => lcfirst($resourceShortName),
  554.                 'in' => 'body',
  555.                 'description' => $description,
  556.             ] + $message;
  557.         }
  558.         return $pathOperation;
  559.     }
  560.     private function hasBodyParameter(array $parameters): bool
  561.     {
  562.         foreach ($parameters as $parameter) {
  563.             if (\array_key_exists('in'$parameter) && 'body' === $parameter['in']) {
  564.                 return true;
  565.             }
  566.         }
  567.         return false;
  568.     }
  569.     private function updateDeleteOperation(bool $v3\ArrayObject $pathOperationstring $resourceShortNamestring $operationTypestring $operationNameResourceMetadata $resourceMetadatastring $resourceClass): \ArrayObject
  570.     {
  571.         $pathOperation['summary'] ?? $pathOperation['summary'] = sprintf('Removes the %s resource.'$resourceShortName);
  572.         $pathOperation['responses'] ?? $pathOperation['responses'] = [
  573.             (string) $resourceMetadata->getTypedOperationAttribute($operationType$operationName'status''204') => ['description' => sprintf('%s resource deleted'$resourceShortName)],
  574.             '404' => ['description' => 'Resource not found'],
  575.         ];
  576.         return $this->addItemOperationParameters($v3$pathOperation$operationType$operationName$resourceMetadata$resourceClass);
  577.     }
  578.     private function addItemOperationParameters(bool $v3\ArrayObject $pathOperationstring $operationTypestring $operationNameResourceMetadata $resourceMetadatastring $resourceClassbool $isPost false): \ArrayObject
  579.     {
  580.         $identifiers = (array) $resourceMetadata
  581.                 ->getTypedOperationAttribute($operationType$operationName'identifiers', [], false);
  582.         // Auto-generated routes in API Platform < 2.7 are considered as collection, hotfix this as the OpenApi Factory supports new operations anyways.
  583.         // this also fixes a bug where we could not create POST item operations in API P 2.6
  584.         if (OperationType::ITEM === $operationType && $isPost) {
  585.             $operationType OperationType::COLLECTION;
  586.         }
  587.         if (!$identifiers && OperationType::COLLECTION !== $operationType) {
  588.             try {
  589.                 $identifiers $this->identifiersExtractor->getIdentifiersFromResourceClass($resourceClass);
  590.             } catch (RuntimeException $e) {
  591.                 // Ignore exception here
  592.             } catch (ResourceClassNotFoundException $e) {
  593.                 if (false === $this->legacyMode) {
  594.                     // Skipping these, swagger is not compatible with post 2.7 resource metadata
  595.                     return $pathOperation;
  596.                 }
  597.                 throw $e;
  598.             }
  599.         }
  600.         if (\count($identifiers) > $resourceMetadata->getItemOperationAttribute($operationName'composite_identifier'truetrue) : false) {
  601.             $identifiers = ['id'];
  602.         }
  603.         if (!$identifiers && OperationType::COLLECTION === $operationType) {
  604.             return $pathOperation;
  605.         }
  606.         if (!isset($pathOperation['parameters'])) {
  607.             $pathOperation['parameters'] = [];
  608.         }
  609.         foreach ($identifiers as $parameterName => $identifier) {
  610.             $parameter = [
  611.                 'name' => \is_string($parameterName) ? $parameterName $identifier,
  612.                 'in' => 'path',
  613.                 'required' => true,
  614.             ];
  615.             $v3 $parameter['schema'] = ['type' => 'string'] : $parameter['type'] = 'string';
  616.             $pathOperation['parameters'][] = $parameter;
  617.         }
  618.         return $pathOperation;
  619.     }
  620.     private function getJsonSchema(bool $v3\ArrayObject $definitionsstring $resourceClassstring $type, ?string $operationType, ?string $operationNamestring $format 'json', ?array $serializerContext nullbool $forceCollection false): Schema
  621.     {
  622.         $schema = new Schema($v3 Schema::VERSION_OPENAPI Schema::VERSION_SWAGGER);
  623.         $schema->setDefinitions($definitions);
  624.         if ($this->jsonSchemaFactory instanceof SchemaFactoryInterface) {
  625.             $operation $operationName ? (new class() extends HttpOperation {})->withName($operationName) : null;
  626.             return $this->jsonSchemaFactory->buildSchema($resourceClass$format$type$operation$schema$serializerContext$forceCollection);
  627.         }
  628.         return $this->jsonSchemaFactory->buildSchema($resourceClass$format$type$operationType$operationName$schema$serializerContext$forceCollection);
  629.     }
  630.     private function computeDoc(bool $v3Documentation $documentation\ArrayObject $definitions\ArrayObject $paths, array $context): array
  631.     {
  632.         $baseUrl $context[self::BASE_URL] ?? $this->defaultContext[self::BASE_URL];
  633.         if ($v3) {
  634.             $docs = ['openapi' => self::OPENAPI_VERSION];
  635.             if ('/' !== $baseUrl && '' !== $baseUrl) {
  636.                 $docs['servers'] = [['url' => $baseUrl]];
  637.             }
  638.         } else {
  639.             $docs = [
  640.                 'swagger' => self::SWAGGER_VERSION,
  641.                 'basePath' => $baseUrl,
  642.             ];
  643.         }
  644.         $docs += [
  645.             'info' => [
  646.                 'title' => $documentation->getTitle(),
  647.                 'version' => $documentation->getVersion(),
  648.             ],
  649.             'paths' => $paths,
  650.         ];
  651.         if ('' !== $description $documentation->getDescription()) {
  652.             $docs['info']['description'] = $description;
  653.         }
  654.         $securityDefinitions = [];
  655.         $security = [];
  656.         if ($this->oauthEnabled) {
  657.             $oauthAttributes = [
  658.                 'authorizationUrl' => $this->oauthAuthorizationUrl,
  659.                 'scopes' => new \ArrayObject($this->oauthScopes),
  660.             ];
  661.             if ($this->oauthTokenUrl) {
  662.                 $oauthAttributes['tokenUrl'] = $this->oauthTokenUrl;
  663.             }
  664.             $securityDefinitions['oauth'] = [
  665.                 'type' => $this->oauthType,
  666.                 'description' => sprintf(
  667.                     'OAuth 2.0 %s Grant',
  668.                     strtolower(preg_replace('/[A-Z]/'' \\0'lcfirst($this->oauthFlow)))
  669.                 ),
  670.             ];
  671.             if ($v3) {
  672.                 $securityDefinitions['oauth']['flows'] = [
  673.                     $this->oauthFlow => $oauthAttributes,
  674.                 ];
  675.             } else {
  676.                 $securityDefinitions['oauth']['flow'] = $this->oauthFlow;
  677.                 $securityDefinitions['oauth'] = array_merge($securityDefinitions['oauth'], $oauthAttributes);
  678.             }
  679.             $security[] = ['oauth' => []];
  680.         }
  681.         foreach ($this->apiKeys as $key => $apiKey) {
  682.             $name $apiKey['name'];
  683.             $type $apiKey['type'];
  684.             $securityDefinitions[$key] = [
  685.                 'type' => 'apiKey',
  686.                 'in' => $type,
  687.                 'description' => sprintf('Value for the %s %s'$name'query' === $type sprintf('%s parameter'$type) : $type),
  688.                 'name' => $name,
  689.             ];
  690.             $security[] = [$key => []];
  691.         }
  692.         if ($securityDefinitions && $security) { // @phpstan-ignore-line false positive
  693.             $docs['security'] = $security;
  694.             if (!$v3) {
  695.                 $docs['securityDefinitions'] = $securityDefinitions;
  696.             }
  697.         }
  698.         if ($v3) {
  699.             if (\count($definitions) + \count($securityDefinitions)) {
  700.                 $docs['components'] = [];
  701.                 if (\count($definitions)) {
  702.                     $docs['components']['schemas'] = $definitions;
  703.                 }
  704.                 if (\count($securityDefinitions)) {
  705.                     $docs['components']['securitySchemes'] = $securityDefinitions;
  706.                 }
  707.             }
  708.         } elseif (\count($definitions) > 0) {
  709.             $docs['definitions'] = $definitions;
  710.         }
  711.         return $docs;
  712.     }
  713.     /**
  714.      * Gets parameters corresponding to enabled filters.
  715.      */
  716.     private function getFiltersParameters(bool $v3string $resourceClassstring $operationNameResourceMetadata $resourceMetadata): array
  717.     {
  718.         if (null === $this->filterLocator) {
  719.             return [];
  720.         }
  721.         $parameters = [];
  722.         $resourceFilters $resourceMetadata->getCollectionOperationAttribute($operationName'filters', [], true);
  723.         foreach ($resourceFilters as $filterId) {
  724.             if (!$filter $this->getFilter($filterId)) {
  725.                 continue;
  726.             }
  727.             foreach ($filter->getDescription($resourceClass) as $name => $data) {
  728.                 $parameter = [
  729.                     'name' => $name,
  730.                     'in' => 'query',
  731.                     'required' => $data['required'],
  732.                 ];
  733.                 $type \in_array($data['type'], Type::$builtinTypestrue) ? $this->jsonSchemaTypeFactory->getType(new Type($data['type'], falsenull$data['is_collection'] ?? false)) : ['type' => 'string'];
  734.                 $v3 $parameter['schema'] = $type $parameter += $type;
  735.                 if ($v3 && isset($data['schema'])) {
  736.                     $parameter['schema'] = $data['schema'];
  737.                 }
  738.                 if ('array' === ($type['type'] ?? '')) {
  739.                     $deepObject \in_array($data['type'], [Type::BUILTIN_TYPE_ARRAYType::BUILTIN_TYPE_OBJECT], true);
  740.                     if ($v3) {
  741.                         $parameter['style'] = $deepObject 'deepObject' 'form';
  742.                         $parameter['explode'] = true;
  743.                     } else {
  744.                         $parameter['collectionFormat'] = $deepObject 'csv' 'multi';
  745.                     }
  746.                 }
  747.                 $key $v3 'openapi' 'swagger';
  748.                 if (isset($data[$key])) {
  749.                     $parameter $data[$key] + $parameter;
  750.                 }
  751.                 $parameters[] = $parameter;
  752.             }
  753.         }
  754.         return $parameters;
  755.     }
  756.     /**
  757.      * {@inheritdoc}
  758.      */
  759.     public function supportsNormalization($data$format null, array $context = []): bool
  760.     {
  761.         return self::FORMAT === $format && ($data instanceof Documentation || $this->openApiNormalizer && $data instanceof OpenApi);
  762.     }
  763.     /**
  764.      * {@inheritdoc}
  765.      */
  766.     public function hasCacheableSupportsMethod(): bool
  767.     {
  768.         return true;
  769.     }
  770.     private function flattenMimeTypes(array $responseFormats): array
  771.     {
  772.         $responseMimeTypes = [];
  773.         foreach ($responseFormats as $responseFormat => $mimeTypes) {
  774.             foreach ($mimeTypes as $mimeType) {
  775.                 $responseMimeTypes[$mimeType] = $responseFormat;
  776.             }
  777.         }
  778.         return $responseMimeTypes;
  779.     }
  780.     /**
  781.      * https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.0.md#linkObject.
  782.      */
  783.     private function getLinkObject(string $resourceClassstring $operationIdstring $path): array
  784.     {
  785.         $linkObject $identifiers = [];
  786.         foreach ($this->propertyNameCollectionFactory->create($resourceClass) as $propertyName) {
  787.             $propertyMetadata $this->propertyMetadataFactory->create($resourceClass$propertyName);
  788.             if (!$propertyMetadata->isIdentifier()) {
  789.                 continue;
  790.             }
  791.             $linkObject['parameters'][$propertyName] = sprintf('$response.body#/%s'$propertyName);
  792.             $identifiers[] = $propertyName;
  793.         }
  794.         if (!$linkObject) {
  795.             return [];
  796.         }
  797.         $linkObject['operationId'] = $operationId;
  798.         $linkObject['description'] = === \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);
  799.         return $linkObject;
  800.     }
  801. }