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

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\OperationAwareFormatsProviderInterface;
  16. use ApiPlatform\Core\Api\OperationMethodResolverInterface;
  17. use ApiPlatform\Core\Api\OperationType;
  18. use ApiPlatform\Core\Api\ResourceClassResolverInterface;
  19. use ApiPlatform\Core\Api\UrlGeneratorInterface;
  20. use ApiPlatform\Core\Documentation\Documentation;
  21. use ApiPlatform\Core\Exception\ResourceClassNotFoundException;
  22. use ApiPlatform\Core\JsonSchema\Schema;
  23. use ApiPlatform\Core\JsonSchema\SchemaFactory;
  24. use ApiPlatform\Core\JsonSchema\SchemaFactoryInterface;
  25. use ApiPlatform\Core\JsonSchema\TypeFactory;
  26. use ApiPlatform\Core\JsonSchema\TypeFactoryInterface;
  27. use ApiPlatform\Core\Metadata\Property\Factory\PropertyMetadataFactoryInterface;
  28. use ApiPlatform\Core\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface;
  29. use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface;
  30. use ApiPlatform\Core\Metadata\Resource\ResourceMetadata;
  31. use ApiPlatform\Core\Operation\Factory\SubresourceOperationFactoryInterface;
  32. use ApiPlatform\Core\PathResolver\OperationPathResolverInterface;
  33. use Psr\Container\ContainerInterface;
  34. use Symfony\Component\PropertyInfo\Type;
  35. use Symfony\Component\Serializer\NameConverter\NameConverterInterface;
  36. use Symfony\Component\Serializer\Normalizer\CacheableSupportsMethodInterface;
  37. use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
  38. /**
  39.  * Generates an OpenAPI specification (formerly known as Swagger). OpenAPI v2 and v3 are supported.
  40.  *
  41.  * @author Amrouche Hamza <hamza.simperfit@gmail.com>
  42.  * @author Teoh Han Hui <teohhanhui@gmail.com>
  43.  * @author Kévin Dunglas <dunglas@gmail.com>
  44.  * @author Anthony GRASSIOT <antograssiot@free.fr>
  45.  */
  46. final class DocumentationNormalizer implements NormalizerInterfaceCacheableSupportsMethodInterface
  47. {
  48.     use FilterLocatorTrait;
  49.     public const FORMAT 'json';
  50.     public const BASE_URL 'base_url';
  51.     public const SPEC_VERSION 'spec_version';
  52.     public const OPENAPI_VERSION '3.0.2';
  53.     public const SWAGGER_DEFINITION_NAME 'swagger_definition_name';
  54.     public const SWAGGER_VERSION '2.0';
  55.     /**
  56.      * @deprecated
  57.      */
  58.     public const ATTRIBUTE_NAME 'swagger_context';
  59.     private $resourceMetadataFactory;
  60.     private $propertyNameCollectionFactory;
  61.     private $propertyMetadataFactory;
  62.     private $operationMethodResolver;
  63.     private $operationPathResolver;
  64.     private $oauthEnabled;
  65.     private $oauthType;
  66.     private $oauthFlow;
  67.     private $oauthTokenUrl;
  68.     private $oauthAuthorizationUrl;
  69.     private $oauthScopes;
  70.     private $apiKeys;
  71.     private $subresourceOperationFactory;
  72.     private $paginationEnabled;
  73.     private $paginationPageParameterName;
  74.     private $clientItemsPerPage;
  75.     private $itemsPerPageParameterName;
  76.     private $paginationClientEnabled;
  77.     private $paginationClientEnabledParameterName;
  78.     private $formats;
  79.     private $formatsProvider;
  80.     /**
  81.      * @var SchemaFactoryInterface
  82.      */
  83.     private $jsonSchemaFactory;
  84.     /**
  85.      * @var TypeFactoryInterface
  86.      */
  87.     private $jsonSchemaTypeFactory;
  88.     private $defaultContext = [
  89.         self::BASE_URL => '/',
  90.         ApiGatewayNormalizer::API_GATEWAY => false,
  91.     ];
  92.     /**
  93.      * @param SchemaFactoryInterface|ResourceClassResolverInterface|null $jsonSchemaFactory
  94.      * @param ContainerInterface|FilterCollection|null                   $filterLocator
  95.      * @param array|OperationAwareFormatsProviderInterface               $formats
  96.      * @param mixed|null                                                 $jsonSchemaTypeFactory
  97.      * @param int[]                                                      $swaggerVersions
  98.      */
  99.     public function __construct(ResourceMetadataFactoryInterface $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])
  100.     {
  101.         if ($jsonSchemaTypeFactory instanceof OperationMethodResolverInterface) {
  102.             @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);
  103.             $this->operationMethodResolver $jsonSchemaTypeFactory;
  104.             $this->jsonSchemaTypeFactory = new TypeFactory();
  105.         } else {
  106.             $this->jsonSchemaTypeFactory $jsonSchemaTypeFactory ?? new TypeFactory();
  107.         }
  108.         if ($jsonSchemaFactory instanceof ResourceClassResolverInterface) {
  109.             @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);
  110.         }
  111.         if (null === $jsonSchemaFactory || $jsonSchemaFactory instanceof ResourceClassResolverInterface) {
  112.             $jsonSchemaFactory = new SchemaFactory($this->jsonSchemaTypeFactory$resourceMetadataFactory$propertyNameCollectionFactory$propertyMetadataFactory$nameConverter);
  113.             $this->jsonSchemaTypeFactory->setSchemaFactory($jsonSchemaFactory);
  114.         }
  115.         $this->jsonSchemaFactory $jsonSchemaFactory;
  116.         if ($nameConverter) {
  117.             @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);
  118.         }
  119.         if ($urlGenerator) {
  120.             @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);
  121.         }
  122.         if ($formats instanceof FormatsProviderInterface) {
  123.             @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);
  124.             $this->formatsProvider $formats;
  125.         } else {
  126.             $this->formats $formats;
  127.         }
  128.         $this->setFilterLocator($filterLocatortrue);
  129.         $this->resourceMetadataFactory $resourceMetadataFactory;
  130.         $this->propertyNameCollectionFactory $propertyNameCollectionFactory;
  131.         $this->propertyMetadataFactory $propertyMetadataFactory;
  132.         $this->operationPathResolver $operationPathResolver;
  133.         $this->oauthEnabled $oauthEnabled;
  134.         $this->oauthType $oauthType;
  135.         $this->oauthFlow $oauthFlow;
  136.         $this->oauthTokenUrl $oauthTokenUrl;
  137.         $this->oauthAuthorizationUrl $oauthAuthorizationUrl;
  138.         $this->oauthScopes $oauthScopes;
  139.         $this->subresourceOperationFactory $subresourceOperationFactory;
  140.         $this->paginationEnabled $paginationEnabled;
  141.         $this->paginationPageParameterName $paginationPageParameterName;
  142.         $this->apiKeys $apiKeys;
  143.         $this->clientItemsPerPage $clientItemsPerPage;
  144.         $this->itemsPerPageParameterName $itemsPerPageParameterName;
  145.         $this->paginationClientEnabled $paginationClientEnabled;
  146.         $this->paginationClientEnabledParameterName $paginationClientEnabledParameterName;
  147.         $this->defaultContext[self::SPEC_VERSION] = $swaggerVersions[0] ?? 2;
  148.         $this->defaultContext array_merge($this->defaultContext$defaultContext);
  149.     }
  150.     /**
  151.      * {@inheritdoc}
  152.      */
  153.     public function normalize($object$format null, array $context = [])
  154.     {
  155.         $v3 === ($context['spec_version'] ?? $this->defaultContext['spec_version']) && !($context['api_gateway'] ?? $this->defaultContext['api_gateway']);
  156.         $definitions = new \ArrayObject();
  157.         $paths = new \ArrayObject();
  158.         $links = new \ArrayObject();
  159.         foreach ($object->getResourceNameCollection() as $resourceClass) {
  160.             $resourceMetadata $this->resourceMetadataFactory->create($resourceClass);
  161.             $resourceShortName $resourceMetadata->getShortName();
  162.             // Items needs to be parsed first to be able to reference the lines from the collection operation
  163.             $this->addPaths($v3$paths$definitions$resourceClass$resourceShortName$resourceMetadataOperationType::ITEM$links);
  164.             $this->addPaths($v3$paths$definitions$resourceClass$resourceShortName$resourceMetadataOperationType::COLLECTION$links);
  165.             if (null === $this->subresourceOperationFactory) {
  166.                 continue;
  167.             }
  168.             foreach ($this->subresourceOperationFactory->create($resourceClass) as $operationId => $subresourceOperation) {
  169.                 $method $resourceMetadata->getTypedOperationAttribute(OperationType::SUBRESOURCE$subresourceOperation['operation_name'], 'method''GET');
  170.                 $paths[$this->getPath($subresourceOperation['shortNames'][0], $subresourceOperation['route_name'], $subresourceOperationOperationType::SUBRESOURCE)][strtolower($method)] = $this->addSubresourceOperation($v3$subresourceOperation$definitions$operationId$resourceMetadata);
  171.             }
  172.         }
  173.         $definitions->ksort();
  174.         $paths->ksort();
  175.         return $this->computeDoc($v3$object$definitions$paths$context);
  176.     }
  177.     /**
  178.      * Updates the list of entries in the paths collection.
  179.      */
  180.     private function addPaths(bool $v3, \ArrayObject $paths, \ArrayObject $definitionsstring $resourceClassstring $resourceShortNameResourceMetadata $resourceMetadatastring $operationType, \ArrayObject $links)
  181.     {
  182.         if (null === $operations OperationType::COLLECTION === $operationType $resourceMetadata->getCollectionOperations() : $resourceMetadata->getItemOperations()) {
  183.             return;
  184.         }
  185.         foreach ($operations as $operationName => $operation) {
  186.             $path $this->getPath($resourceShortName$operationName$operation$operationType);
  187.             if ($this->operationMethodResolver) {
  188.                 $method OperationType::ITEM === $operationType $this->operationMethodResolver->getItemOperationMethod($resourceClass$operationName) : $this->operationMethodResolver->getCollectionOperationMethod($resourceClass$operationName);
  189.             } else {
  190.                 $method $resourceMetadata->getTypedOperationAttribute($operationType$operationName'method''GET');
  191.             }
  192.             $paths[$path][strtolower($method)] = $this->getPathOperation($v3$operationName$operation$method$operationType$resourceClass$resourceMetadata$definitions$links);
  193.         }
  194.     }
  195.     /**
  196.      * Gets the path for an operation.
  197.      *
  198.      * If the path ends with the optional _format parameter, it is removed
  199.      * as optional path parameters are not yet supported.
  200.      *
  201.      * @see https://github.com/OAI/OpenAPI-Specification/issues/93
  202.      */
  203.     private function getPath(string $resourceShortNamestring $operationName, array $operationstring $operationType): string
  204.     {
  205.         $path $this->operationPathResolver->resolveOperationPath($resourceShortName$operation$operationType$operationName);
  206.         if ('.{_format}' === substr($path, -10)) {
  207.             $path substr($path0, -10);
  208.         }
  209.         return $path;
  210.     }
  211.     /**
  212.      * Gets a path Operation Object.
  213.      *
  214.      * @see https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#operation-object
  215.      */
  216.     private function getPathOperation(bool $v3string $operationName, array $operationstring $methodstring $operationTypestring $resourceClassResourceMetadata $resourceMetadata, \ArrayObject $definitions, \ArrayObject $links): \ArrayObject
  217.     {
  218.         $pathOperation = new \ArrayObject($operation[$v3 'openapi_context' 'swagger_context'] ?? []);
  219.         $resourceShortName $resourceMetadata->getShortName();
  220.         $pathOperation['tags'] ?? $pathOperation['tags'] = [$resourceShortName];
  221.         $pathOperation['operationId'] ?? $pathOperation['operationId'] = lcfirst($operationName).ucfirst($resourceShortName).ucfirst($operationType);
  222.         if ($v3 && 'GET' === $method && OperationType::ITEM === $operationType && $link $this->getLinkObject($resourceClass$pathOperation['operationId'], $this->getPath($resourceShortName$operationName$operation$operationType))) {
  223.             $links[$pathOperation['operationId']] = $link;
  224.         }
  225.         if ($resourceMetadata->getTypedOperationAttribute($operationType$operationName'deprecation_reason'nulltrue)) {
  226.             $pathOperation['deprecated'] = true;
  227.         }
  228.         if (null === $this->formatsProvider) {
  229.             $requestFormats $resourceMetadata->getTypedOperationAttribute($operationType$operationName'input_formats', [], true);
  230.             $responseFormats $resourceMetadata->getTypedOperationAttribute($operationType$operationName'output_formats', [], true);
  231.         } else {
  232.             $requestFormats $responseFormats $this->formatsProvider->getFormatsFromOperation($resourceClass$operationName$operationType);
  233.         }
  234.         $requestMimeTypes $this->flattenMimeTypes($requestFormats);
  235.         $responseMimeTypes $this->flattenMimeTypes($responseFormats);
  236.         switch ($method) {
  237.             case 'GET':
  238.                 return $this->updateGetOperation($v3$pathOperation$responseMimeTypes$operationType$resourceMetadata$resourceClass$resourceShortName$operationName$definitions);
  239.             case 'POST':
  240.                 return $this->updatePostOperation($v3$pathOperation$requestMimeTypes$responseMimeTypes$operationType$resourceMetadata$resourceClass$resourceShortName$operationName$definitions$links);
  241.             case 'PATCH':
  242.                 $pathOperation['summary'] ?? $pathOperation['summary'] = sprintf('Updates the %s resource.'$resourceShortName);
  243.             // no break
  244.             case 'PUT':
  245.                 return $this->updatePutOperation($v3$pathOperation$requestMimeTypes$responseMimeTypes$operationType$resourceMetadata$resourceClass$resourceShortName$operationName$definitions);
  246.             case 'DELETE':
  247.                 return $this->updateDeleteOperation($v3$pathOperation$resourceShortName$operationType$operationName$resourceMetadata);
  248.         }
  249.         return $pathOperation;
  250.     }
  251.     /**
  252.      * @return array the update message as first value, and if the schema is defined as second
  253.      */
  254.     private function addSchemas(bool $v3, array $message, \ArrayObject $definitionsstring $resourceClassstring $operationTypestring $operationName, array $mimeTypesstring $type Schema::TYPE_OUTPUTbool $forceCollection false): array
  255.     {
  256.         if (!$v3) {
  257.             $jsonSchema $this->getJsonSchema($v3$definitions$resourceClass$type$operationType$operationName'json'null$forceCollection);
  258.             if (!$jsonSchema->isDefined()) {
  259.                 return [$messagefalse];
  260.             }
  261.             $message['schema'] = $jsonSchema->getArrayCopy(false);
  262.             return [$messagetrue];
  263.         }
  264.         foreach ($mimeTypes as $mimeType => $format) {
  265.             $jsonSchema $this->getJsonSchema($v3$definitions$resourceClass$type$operationType$operationName$formatnull$forceCollection);
  266.             if (!$jsonSchema->isDefined()) {
  267.                 return [$messagefalse];
  268.             }
  269.             $message['content'][$mimeType] = ['schema' => $jsonSchema->getArrayCopy(false)];
  270.         }
  271.         return [$messagetrue];
  272.     }
  273.     private function updateGetOperation(bool $v3, \ArrayObject $pathOperation, array $mimeTypesstring $operationTypeResourceMetadata $resourceMetadatastring $resourceClassstring $resourceShortNamestring $operationName, \ArrayObject $definitions): \ArrayObject
  274.     {
  275.         $successStatus = (string) $resourceMetadata->getTypedOperationAttribute($operationType$operationName'status''200');
  276.         if (!$v3) {
  277.             $pathOperation['produces'] ?? $pathOperation['produces'] = array_keys($mimeTypes);
  278.         }
  279.         if (OperationType::COLLECTION === $operationType) {
  280.             $outputResourseShortName $resourceMetadata->getCollectionOperations()[$operationName]['output']['name'] ?? $resourceShortName;
  281.             $pathOperation['summary'] ?? $pathOperation['summary'] = sprintf('Retrieves the collection of %s resources.'$outputResourseShortName);
  282.             $successResponse = ['description' => sprintf('%s collection response'$outputResourseShortName)];
  283.             [$successResponse] = $this->addSchemas($v3$successResponse$definitions$resourceClass$operationType$operationName$mimeTypes);
  284.             $pathOperation['responses'] ?? $pathOperation['responses'] = [$successStatus => $successResponse];
  285.             $pathOperation['parameters'] ?? $pathOperation['parameters'] = $this->getFiltersParameters($v3$resourceClass$operationName$resourceMetadata);
  286.             $this->addPaginationParameters($v3$resourceMetadataOperationType::COLLECTION$operationName$pathOperation);
  287.             return $pathOperation;
  288.         }
  289.         $outputResourseShortName $resourceMetadata->getItemOperations()[$operationName]['output']['name'] ?? $resourceShortName;
  290.         $pathOperation['summary'] ?? $pathOperation['summary'] = sprintf('Retrieves a %s resource.'$outputResourseShortName);
  291.         $pathOperation $this->addItemOperationParameters($v3$pathOperation);
  292.         $successResponse = ['description' => sprintf('%s resource response'$outputResourseShortName)];
  293.         [$successResponse] = $this->addSchemas($v3$successResponse$definitions$resourceClass$operationType$operationName$mimeTypes);
  294.         $pathOperation['responses'] ?? $pathOperation['responses'] = [
  295.             $successStatus => $successResponse,
  296.             '404' => ['description' => 'Resource not found'],
  297.         ];
  298.         return $pathOperation;
  299.     }
  300.     private function addPaginationParameters(bool $v3ResourceMetadata $resourceMetadatastring $operationTypestring $operationName, \ArrayObject $pathOperation)
  301.     {
  302.         if ($this->paginationEnabled && $resourceMetadata->getTypedOperationAttribute($operationType$operationName'pagination_enabled'truetrue)) {
  303.             $paginationParameter = [
  304.                 'name' => $this->paginationPageParameterName,
  305.                 'in' => 'query',
  306.                 'required' => false,
  307.                 'description' => 'The collection page number',
  308.             ];
  309.             $v3 $paginationParameter['schema'] = [
  310.                 'type' => 'integer',
  311.                 'default' => 1,
  312.             ] : $paginationParameter['type'] = 'integer';
  313.             $pathOperation['parameters'][] = $paginationParameter;
  314.             if ($resourceMetadata->getTypedOperationAttribute($operationType$operationName'pagination_client_items_per_page'$this->clientItemsPerPagetrue)) {
  315.                 $itemPerPageParameter = [
  316.                     'name' => $this->itemsPerPageParameterName,
  317.                     'in' => 'query',
  318.                     'required' => false,
  319.                     'description' => 'The number of items per page',
  320.                 ];
  321.                 if ($v3) {
  322.                     $itemPerPageParameter['schema'] = [
  323.                         'type' => 'integer',
  324.                         'default' => $resourceMetadata->getTypedOperationAttribute($operationType$operationName'pagination_items_per_page'30true),
  325.                         'minimum' => 0,
  326.                     ];
  327.                     $maxItemsPerPage $resourceMetadata->getTypedOperationAttribute($operationType$operationName'maximum_items_per_page'nulltrue);
  328.                     if (null !== $maxItemsPerPage) {
  329.                         @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);
  330.                     }
  331.                     $maxItemsPerPage $resourceMetadata->getTypedOperationAttribute($operationType$operationName'pagination_maximum_items_per_page'$maxItemsPerPagetrue);
  332.                     if (null !== $maxItemsPerPage) {
  333.                         $itemPerPageParameter['schema']['maximum'] = $maxItemsPerPage;
  334.                     }
  335.                 } else {
  336.                     $itemPerPageParameter['type'] = 'integer';
  337.                 }
  338.                 $pathOperation['parameters'][] = $itemPerPageParameter;
  339.             }
  340.         }
  341.         if ($this->paginationEnabled && $resourceMetadata->getTypedOperationAttribute($operationType$operationName'pagination_client_enabled'$this->paginationClientEnabledtrue)) {
  342.             $paginationEnabledParameter = [
  343.                 'name' => $this->paginationClientEnabledParameterName,
  344.                 'in' => 'query',
  345.                 'required' => false,
  346.                 'description' => 'Enable or disable pagination',
  347.             ];
  348.             $v3 $paginationEnabledParameter['schema'] = ['type' => 'boolean'] : $paginationEnabledParameter['type'] = 'boolean';
  349.             $pathOperation['parameters'][] = $paginationEnabledParameter;
  350.         }
  351.     }
  352.     /**
  353.      * @throws ResourceClassNotFoundException
  354.      */
  355.     private function addSubresourceOperation(bool $v3, array $subresourceOperation, \ArrayObject $definitionsstring $operationIdResourceMetadata $resourceMetadata): \ArrayObject
  356.     {
  357.         $operationName 'get'// TODO: we might want to extract that at some point to also support other subresource operations
  358.         $collection $subresourceOperation['collection'] ?? false;
  359.         $subResourceMetadata $this->resourceMetadataFactory->create($subresourceOperation['resource_class']);
  360.         $pathOperation = new \ArrayObject([]);
  361.         $pathOperation['tags'] = $subresourceOperation['shortNames'];
  362.         $pathOperation['operationId'] = $operationId;
  363.         $pathOperation['summary'] = sprintf('Retrieves %s%s resource%s.'$subresourceOperation['collection'] ? 'the collection of ' 'a '$subresourceOperation['shortNames'][0], $subresourceOperation['collection'] ? 's' '');
  364.         if (null === $this->formatsProvider) {
  365.             // TODO: Subresource operation metadata aren't available by default, for now we have to fallback on default formats.
  366.             // TODO: A better approach would be to always populate the subresource operation array.
  367.             $responseFormats $this
  368.                 ->resourceMetadataFactory
  369.                 ->create($subresourceOperation['resource_class'])
  370.                 ->getTypedOperationAttribute(OperationType::SUBRESOURCE$operationName'output_formats'$this->formatstrue);
  371.         } else {
  372.             $responseFormats $this->formatsProvider->getFormatsFromOperation($subresourceOperation['resource_class'], $operationNameOperationType::SUBRESOURCE);
  373.         }
  374.         $mimeTypes $this->flattenMimeTypes($responseFormats);
  375.         if (!$v3) {
  376.             $pathOperation['produces'] = array_keys($mimeTypes);
  377.         }
  378.         $successResponse = [
  379.             'description' => sprintf('%s %s response'$subresourceOperation['shortNames'][0], $collection 'collection' 'resource'),
  380.         ];
  381.         [$successResponse] = $this->addSchemas($v3$successResponse$definitions$subresourceOperation['resource_class'], OperationType::SUBRESOURCE$operationName$mimeTypesSchema::TYPE_OUTPUT$collection);
  382.         $pathOperation['responses'] = ['200' => $successResponse'404' => ['description' => 'Resource not found']];
  383.         // Avoid duplicates parameters when there is a filter on a subresource identifier
  384.         $parametersMemory = [];
  385.         $pathOperation['parameters'] = [];
  386.         foreach ($subresourceOperation['identifiers'] as [$identifier, , $hasIdentifier]) {
  387.             if (true === $hasIdentifier) {
  388.                 $parameter = ['name' => $identifier'in' => 'path''required' => true];
  389.                 $v3 $parameter['schema'] = ['type' => 'string'] : $parameter['type'] = 'string';
  390.                 $pathOperation['parameters'][] = $parameter;
  391.                 $parametersMemory[] = $identifier;
  392.             }
  393.         }
  394.         if ($parameters $this->getFiltersParameters($v3$subresourceOperation['resource_class'], $operationName$subResourceMetadata)) {
  395.             foreach ($parameters as $parameter) {
  396.                 if (!\in_array($parameter['name'], $parametersMemorytrue)) {
  397.                     $pathOperation['parameters'][] = $parameter;
  398.                 }
  399.             }
  400.         }
  401.         if ($subresourceOperation['collection']) {
  402.             $this->addPaginationParameters($v3$subResourceMetadataOperationType::SUBRESOURCE$subresourceOperation['operation_name'], $pathOperation);
  403.         }
  404.         return $pathOperation;
  405.     }
  406.     private function updatePostOperation(bool $v3, \ArrayObject $pathOperation, array $requestMimeTypes, array $responseMimeTypesstring $operationTypeResourceMetadata $resourceMetadatastring $resourceClassstring $resourceShortNamestring $operationName, \ArrayObject $definitions, \ArrayObject $links): \ArrayObject
  407.     {
  408.         if (!$v3) {
  409.             $pathOperation['consumes'] ?? $pathOperation['consumes'] = array_keys($requestMimeTypes);
  410.             $pathOperation['produces'] ?? $pathOperation['produces'] = array_keys($responseMimeTypes);
  411.         }
  412.         $pathOperation['summary'] ?? $pathOperation['summary'] = sprintf('Creates a %s resource.'$resourceShortName);
  413.         if (OperationType::ITEM === $operationType) {
  414.             $pathOperation $this->addItemOperationParameters($v3$pathOperation);
  415.         }
  416.         $successResponse = ['description' => sprintf('%s resource created'$resourceShortName)];
  417.         [$successResponse$defined] = $this->addSchemas($v3$successResponse$definitions$resourceClass$operationType$operationName$responseMimeTypes);
  418.         if ($defined && $v3 && ($links[$key 'get'.ucfirst($resourceShortName).ucfirst(OperationType::ITEM)] ?? null)) {
  419.             $successResponse['links'] = [ucfirst($key) => $links[$key]];
  420.         }
  421.         $pathOperation['responses'] ?? $pathOperation['responses'] = [
  422.             (string) $resourceMetadata->getTypedOperationAttribute($operationType$operationName'status''201') => $successResponse,
  423.             '400' => ['description' => 'Invalid input'],
  424.             '404' => ['description' => 'Resource not found'],
  425.         ];
  426.         return $this->addRequestBody($v3$pathOperation$definitions$resourceClass$resourceShortName$operationType$operationName$requestMimeTypes);
  427.     }
  428.     private function updatePutOperation(bool $v3, \ArrayObject $pathOperation, array $requestMimeTypes, array $responseMimeTypesstring $operationTypeResourceMetadata $resourceMetadatastring $resourceClassstring $resourceShortNamestring $operationName, \ArrayObject $definitions): \ArrayObject
  429.     {
  430.         if (!$v3) {
  431.             $pathOperation['consumes'] ?? $pathOperation['consumes'] = array_keys($requestMimeTypes);
  432.             $pathOperation['produces'] ?? $pathOperation['produces'] = array_keys($responseMimeTypes);
  433.         }
  434.         $pathOperation['summary'] ?? $pathOperation['summary'] = sprintf('Replaces the %s resource.'$resourceShortName);
  435.         $pathOperation $this->addItemOperationParameters($v3$pathOperation);
  436.         $successResponse = ['description' => sprintf('%s resource updated'$resourceShortName)];
  437.         [$successResponse] = $this->addSchemas($v3$successResponse$definitions$resourceClass$operationType$operationName$responseMimeTypes);
  438.         $pathOperation['responses'] ?? $pathOperation['responses'] = [
  439.             (string) $resourceMetadata->getTypedOperationAttribute($operationType$operationName'status''200') => $successResponse,
  440.             '400' => ['description' => 'Invalid input'],
  441.             '404' => ['description' => 'Resource not found'],
  442.         ];
  443.         return $this->addRequestBody($v3$pathOperation$definitions$resourceClass$resourceShortName$operationType$operationName$requestMimeTypestrue);
  444.     }
  445.     private function addRequestBody(bool $v3, \ArrayObject $pathOperation, \ArrayObject $definitionsstring $resourceClassstring $resourceShortNamestring $operationTypestring $operationName, array $requestMimeTypesbool $put false)
  446.     {
  447.         if (isset($pathOperation['requestBody'])) {
  448.             return $pathOperation;
  449.         }
  450.         [$message$defined] = $this->addSchemas($v3, [], $definitions$resourceClass$operationType$operationName$requestMimeTypesSchema::TYPE_INPUT);
  451.         if (!$defined) {
  452.             return $pathOperation;
  453.         }
  454.         $description sprintf('The %s %s resource'$put 'updated' 'new'$resourceShortName);
  455.         if ($v3) {
  456.             $pathOperation['requestBody'] = $message + ['description' => $description];
  457.             return $pathOperation;
  458.         }
  459.         if (!$this->hasBodyParameter($pathOperation['parameters'] ?? [])) {
  460.             $pathOperation['parameters'][] = [
  461.                 'name' => lcfirst($resourceShortName),
  462.                 'in' => 'body',
  463.                 'description' => $description,
  464.             ] + $message;
  465.         }
  466.         return $pathOperation;
  467.     }
  468.     private function hasBodyParameter(array $parameters): bool
  469.     {
  470.         foreach ($parameters as $parameter) {
  471.             if (\array_key_exists('in'$parameter) && 'body' === $parameter['in']) {
  472.                 return true;
  473.             }
  474.         }
  475.         return false;
  476.     }
  477.     private function updateDeleteOperation(bool $v3, \ArrayObject $pathOperationstring $resourceShortNamestring $operationTypestring $operationNameResourceMetadata $resourceMetadata): \ArrayObject
  478.     {
  479.         $pathOperation['summary'] ?? $pathOperation['summary'] = sprintf('Removes the %s resource.'$resourceShortName);
  480.         $pathOperation['responses'] ?? $pathOperation['responses'] = [
  481.             (string) $resourceMetadata->getTypedOperationAttribute($operationType$operationName'status''204') => ['description' => sprintf('%s resource deleted'$resourceShortName)],
  482.             '404' => ['description' => 'Resource not found'],
  483.         ];
  484.         return $this->addItemOperationParameters($v3$pathOperation);
  485.     }
  486.     private function addItemOperationParameters(bool $v3, \ArrayObject $pathOperation): \ArrayObject
  487.     {
  488.         $parameter = [
  489.             'name' => 'id',
  490.             'in' => 'path',
  491.             'required' => true,
  492.         ];
  493.         $v3 $parameter['schema'] = ['type' => 'string'] : $parameter['type'] = 'string';
  494.         $pathOperation['parameters'] ?? $pathOperation['parameters'] = [$parameter];
  495.         return $pathOperation;
  496.     }
  497.     private function getJsonSchema(bool $v3, \ArrayObject $definitionsstring $resourceClassstring $type, ?string $operationType, ?string $operationNamestring $format 'json', ?array $serializerContext nullbool $forceCollection false): Schema
  498.     {
  499.         $schema = new Schema($v3 Schema::VERSION_OPENAPI Schema::VERSION_SWAGGER);
  500.         $schema->setDefinitions($definitions);
  501.         $this->jsonSchemaFactory->buildSchema($resourceClass$format$type$operationType$operationName$schema$serializerContext$forceCollection);
  502.         return $schema;
  503.     }
  504.     private function computeDoc(bool $v3Documentation $documentation, \ArrayObject $definitions, \ArrayObject $paths, array $context): array
  505.     {
  506.         $baseUrl $context[self::BASE_URL] ?? $this->defaultContext[self::BASE_URL];
  507.         if ($v3) {
  508.             $docs = ['openapi' => self::OPENAPI_VERSION];
  509.             if ('/' !== $baseUrl && '' !== $baseUrl) {
  510.                 $docs['servers'] = [['url' => $baseUrl]];
  511.             }
  512.         } else {
  513.             $docs = [
  514.                 'swagger' => self::SWAGGER_VERSION,
  515.                 'basePath' => $baseUrl,
  516.             ];
  517.         }
  518.         $docs += [
  519.             'info' => [
  520.                 'title' => $documentation->getTitle(),
  521.                 'version' => $documentation->getVersion(),
  522.             ],
  523.             'paths' => $paths,
  524.         ];
  525.         if ('' !== $description $documentation->getDescription()) {
  526.             $docs['info']['description'] = $description;
  527.         }
  528.         $securityDefinitions = [];
  529.         $security = [];
  530.         if ($this->oauthEnabled) {
  531.             $oauthAttributes = [
  532.                 'tokenUrl' => $this->oauthTokenUrl,
  533.                 'authorizationUrl' => $this->oauthAuthorizationUrl,
  534.                 'scopes' => $this->oauthScopes,
  535.             ];
  536.             $securityDefinitions['oauth'] = [
  537.                 'type' => $this->oauthType,
  538.                 'description' => sprintf(
  539.                     'OAuth 2.0 %s Grant',
  540.                     strtolower(preg_replace('/[A-Z]/'' \\0'lcfirst($this->oauthFlow)))
  541.                 ),
  542.             ];
  543.             if ($v3) {
  544.                 $securityDefinitions['oauth']['flows'] = [
  545.                     $this->oauthFlow => $oauthAttributes,
  546.                 ];
  547.             } else {
  548.                 $securityDefinitions['oauth']['flow'] = $this->oauthFlow;
  549.                 $securityDefinitions['oauth'] = array_merge($securityDefinitions['oauth'], $oauthAttributes);
  550.             }
  551.             $security[] = ['oauth' => []];
  552.         }
  553.         foreach ($this->apiKeys as $key => $apiKey) {
  554.             $name $apiKey['name'];
  555.             $type $apiKey['type'];
  556.             $securityDefinitions[$key] = [
  557.                 'type' => 'apiKey',
  558.                 'in' => $type,
  559.                 'description' => sprintf('Value for the %s %s'$name'query' === $type sprintf('%s parameter'$type) : $type),
  560.                 'name' => $name,
  561.             ];
  562.             $security[] = [$key => []];
  563.         }
  564.         if ($v3) {
  565.             if ($securityDefinitions && $security) { // @phpstan-ignore-line false positive
  566.                 $docs['security'] = $security;
  567.             }
  568.         } elseif ($securityDefinitions && $security) { // @phpstan-ignore-line false positive
  569.             $docs['securityDefinitions'] = $securityDefinitions;
  570.             $docs['security'] = $security;
  571.         }
  572.         if ($v3) {
  573.             if (\count($definitions) + \count($securityDefinitions)) {
  574.                 $docs['components'] = [];
  575.                 if (\count($definitions)) {
  576.                     $docs['components']['schemas'] = $definitions;
  577.                 }
  578.                 if (\count($securityDefinitions)) {
  579.                     $docs['components']['securitySchemes'] = $securityDefinitions;
  580.                 }
  581.             }
  582.         } elseif (\count($definitions) > 0) {
  583.             $docs['definitions'] = $definitions;
  584.         }
  585.         return $docs;
  586.     }
  587.     /**
  588.      * Gets parameters corresponding to enabled filters.
  589.      */
  590.     private function getFiltersParameters(bool $v3string $resourceClassstring $operationNameResourceMetadata $resourceMetadata): array
  591.     {
  592.         if (null === $this->filterLocator) {
  593.             return [];
  594.         }
  595.         $parameters = [];
  596.         $resourceFilters $resourceMetadata->getCollectionOperationAttribute($operationName'filters', [], true);
  597.         foreach ($resourceFilters as $filterId) {
  598.             if (!$filter $this->getFilter($filterId)) {
  599.                 continue;
  600.             }
  601.             foreach ($filter->getDescription($resourceClass) as $name => $data) {
  602.                 $parameter = [
  603.                     'name' => $name,
  604.                     'in' => 'query',
  605.                     'required' => $data['required'],
  606.                 ];
  607.                 $type = \in_array($data['type'], Type::$builtinTypestrue) ? $this->jsonSchemaTypeFactory->getType(new Type($data['type'], falsenull$data['is_collection'] ?? false)) : ['type' => 'string'];
  608.                 $v3 $parameter['schema'] = $type $parameter += $type;
  609.                 if ($v3 && isset($data['schema'])) {
  610.                     $parameter['schema'] = $data['schema'];
  611.                 }
  612.                 if ('array' === ($type['type'] ?? '')) {
  613.                     $deepObject = \in_array($data['type'], [Type::BUILTIN_TYPE_ARRAYType::BUILTIN_TYPE_OBJECT], true);
  614.                     if ($v3) {
  615.                         $parameter['style'] = $deepObject 'deepObject' 'form';
  616.                         $parameter['explode'] = true;
  617.                     } else {
  618.                         $parameter['collectionFormat'] = $deepObject 'csv' 'multi';
  619.                     }
  620.                 }
  621.                 $key $v3 'openapi' 'swagger';
  622.                 if (isset($data[$key])) {
  623.                     $parameter $data[$key] + $parameter;
  624.                 }
  625.                 $parameters[] = $parameter;
  626.             }
  627.         }
  628.         return $parameters;
  629.     }
  630.     /**
  631.      * {@inheritdoc}
  632.      */
  633.     public function supportsNormalization($data$format null): bool
  634.     {
  635.         return self::FORMAT === $format && $data instanceof Documentation;
  636.     }
  637.     /**
  638.      * {@inheritdoc}
  639.      */
  640.     public function hasCacheableSupportsMethod(): bool
  641.     {
  642.         return true;
  643.     }
  644.     private function flattenMimeTypes(array $responseFormats): array
  645.     {
  646.         $responseMimeTypes = [];
  647.         foreach ($responseFormats as $responseFormat => $mimeTypes) {
  648.             foreach ($mimeTypes as $mimeType) {
  649.                 $responseMimeTypes[$mimeType] = $responseFormat;
  650.             }
  651.         }
  652.         return $responseMimeTypes;
  653.     }
  654.     /**
  655.      * https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.0.md#linkObject.
  656.      */
  657.     private function getLinkObject(string $resourceClassstring $operationIdstring $path): array
  658.     {
  659.         $linkObject $identifiers = [];
  660.         foreach ($this->propertyNameCollectionFactory->create($resourceClass) as $propertyName) {
  661.             $propertyMetadata $this->propertyMetadataFactory->create($resourceClass$propertyName);
  662.             if (!$propertyMetadata->isIdentifier()) {
  663.                 continue;
  664.             }
  665.             $linkObject['parameters'][$propertyName] = sprintf('$response.body#/%s'$propertyName);
  666.             $identifiers[] = $propertyName;
  667.         }
  668.         if (!$linkObject) {
  669.             return [];
  670.         }
  671.         $linkObject['operationId'] = $operationId;
  672.         $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);
  673.         return $linkObject;
  674.     }
  675. }