vendor/api-platform/core/src/Core/Swagger/Serializer/DocumentationNormalizer.php line 170
<?php/** This file is part of the API Platform project.** (c) Kévin Dunglas <dunglas@gmail.com>** For the full copyright and license information, please view the LICENSE* file that was distributed with this source code.*/declare(strict_types=1);namespace ApiPlatform\Core\Swagger\Serializer;use ApiPlatform\Core\Api\FilterCollection;use ApiPlatform\Core\Api\FilterLocatorTrait;use ApiPlatform\Core\Api\FormatsProviderInterface;use ApiPlatform\Core\Api\IdentifiersExtractorInterface;use ApiPlatform\Core\Api\OperationAwareFormatsProviderInterface;use ApiPlatform\Core\Api\OperationMethodResolverInterface;use ApiPlatform\Core\Api\OperationType;use ApiPlatform\Core\Api\ResourceClassResolverInterface;use ApiPlatform\Core\Api\UrlGeneratorInterface;use ApiPlatform\Core\JsonSchema\SchemaFactory as LegacySchemaFactory;use ApiPlatform\Core\JsonSchema\SchemaFactoryInterface as LegacySchemaFactoryInterface;use ApiPlatform\Core\Metadata\Property\Factory\PropertyMetadataFactoryInterface;use ApiPlatform\Core\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface;use ApiPlatform\Core\Metadata\Resource\ApiResourceToLegacyResourceMetadataTrait;use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface;use ApiPlatform\Core\Metadata\Resource\ResourceMetadata;use ApiPlatform\Core\Operation\Factory\SubresourceOperationFactoryInterface;use ApiPlatform\Documentation\Documentation;use ApiPlatform\Exception\ResourceClassNotFoundException;use ApiPlatform\Exception\RuntimeException;use ApiPlatform\JsonSchema\Schema;use ApiPlatform\JsonSchema\SchemaFactory;use ApiPlatform\JsonSchema\SchemaFactoryInterface;use ApiPlatform\JsonSchema\TypeFactory;use ApiPlatform\JsonSchema\TypeFactoryInterface;use ApiPlatform\Metadata\HttpOperation;use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface;use ApiPlatform\OpenApi\OpenApi;use ApiPlatform\OpenApi\Serializer\ApiGatewayNormalizer;use ApiPlatform\PathResolver\OperationPathResolverInterface;use Psr\Container\ContainerInterface;use Symfony\Component\PropertyInfo\Type;use Symfony\Component\Serializer\NameConverter\NameConverterInterface;use Symfony\Component\Serializer\Normalizer\CacheableSupportsMethodInterface;use Symfony\Component\Serializer\Normalizer\NormalizerInterface;/*** Generates an OpenAPI specification (formerly known as Swagger). OpenAPI v2 and v3 are supported.** @author Amrouche Hamza <hamza.simperfit@gmail.com>* @author Teoh Han Hui <teohhanhui@gmail.com>* @author Kévin Dunglas <dunglas@gmail.com>* @author Anthony GRASSIOT <antograssiot@free.fr>*/final class DocumentationNormalizer implements NormalizerInterface, CacheableSupportsMethodInterface{use ApiResourceToLegacyResourceMetadataTrait;use FilterLocatorTrait;public const FORMAT = 'json';public const BASE_URL = 'base_url';public const SPEC_VERSION = 'spec_version';public const OPENAPI_VERSION = '3.0.2';public const SWAGGER_DEFINITION_NAME = 'swagger_definition_name';public const SWAGGER_VERSION = '2.0';/*** @deprecated*/public const ATTRIBUTE_NAME = 'swagger_context';private $resourceMetadataFactory;private $propertyNameCollectionFactory;private $propertyMetadataFactory;private $operationMethodResolver;private $operationPathResolver;private $oauthEnabled;private $oauthType;private $oauthFlow;private $oauthTokenUrl;private $oauthAuthorizationUrl;private $oauthScopes;private $apiKeys;private $subresourceOperationFactory;private $paginationEnabled;private $paginationPageParameterName;private $clientItemsPerPage;private $itemsPerPageParameterName;private $paginationClientEnabled;private $paginationClientEnabledParameterName;private $formats;private $formatsProvider;/*** @var SchemaFactoryInterface|LegacySchemaFactoryInterface*/private $jsonSchemaFactory;/*** @var TypeFactoryInterface*/private $jsonSchemaTypeFactory;private $defaultContext = [self::BASE_URL => '/',ApiGatewayNormalizer::API_GATEWAY => false,];private $identifiersExtractor;private $openApiNormalizer;private $legacyMode;/*** @param LegacySchemaFactoryInterface|SchemaFactoryInterface|ResourceClassResolverInterface|null $jsonSchemaFactory* @param ContainerInterface|FilterCollection|null $filterLocator* @param array|OperationAwareFormatsProviderInterface $formats* @param mixed|null $jsonSchemaTypeFactory* @param int[] $swaggerVersions* @param mixed $resourceMetadataFactory*/public function __construct($resourceMetadataFactory, PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, $jsonSchemaFactory = null, $jsonSchemaTypeFactory = null, OperationPathResolverInterface $operationPathResolver = null, UrlGeneratorInterface $urlGenerator = null, $filterLocator = null, NameConverterInterface $nameConverter = null, bool $oauthEnabled = false, string $oauthType = '', string $oauthFlow = '', string $oauthTokenUrl = '', string $oauthAuthorizationUrl = '', array $oauthScopes = [], array $apiKeys = [], SubresourceOperationFactoryInterface $subresourceOperationFactory = null, bool $paginationEnabled = true, string $paginationPageParameterName = 'page', bool $clientItemsPerPage = false, string $itemsPerPageParameterName = 'itemsPerPage', $formats = [], bool $paginationClientEnabled = false, string $paginationClientEnabledParameterName = 'pagination', array $defaultContext = [], array $swaggerVersions = [2, 3], IdentifiersExtractorInterface $identifiersExtractor = null, NormalizerInterface $openApiNormalizer = null, bool $legacyMode = false){if ($jsonSchemaTypeFactory instanceof OperationMethodResolverInterface) {@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);$this->operationMethodResolver = $jsonSchemaTypeFactory;$this->jsonSchemaTypeFactory = new TypeFactory();} else {$this->jsonSchemaTypeFactory = $jsonSchemaTypeFactory ?? new TypeFactory();}if ($jsonSchemaFactory instanceof ResourceClassResolverInterface) {@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);}if (null === $jsonSchemaFactory || $jsonSchemaFactory instanceof ResourceClassResolverInterface) {if ($resourceMetadataFactory instanceof ResourceMetadataFactoryInterface) {$jsonSchemaFactory = new LegacySchemaFactory($this->jsonSchemaTypeFactory, $resourceMetadataFactory, $propertyNameCollectionFactory, $propertyMetadataFactory, $nameConverter);} else {$jsonSchemaFactory = new SchemaFactory($this->jsonSchemaTypeFactory, $resourceMetadataFactory, $propertyNameCollectionFactory, $propertyMetadataFactory, $nameConverter);}$this->jsonSchemaTypeFactory->setSchemaFactory($jsonSchemaFactory);}$this->jsonSchemaFactory = $jsonSchemaFactory;if ($nameConverter) {@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);}if ($urlGenerator) {@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);}if ($formats instanceof FormatsProviderInterface) {@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);$this->formatsProvider = $formats;} else {$this->formats = $formats;}$this->setFilterLocator($filterLocator, true);if ($resourceMetadataFactory instanceof ResourceMetadataFactoryInterface) {trigger_deprecation('api-platform/core', '2.7', sprintf('Use "%s" instead of "%s".', ResourceMetadataCollectionFactoryInterface::class, ResourceMetadataFactoryInterface::class));}$this->resourceMetadataFactory = $resourceMetadataFactory;$this->propertyNameCollectionFactory = $propertyNameCollectionFactory;$this->propertyMetadataFactory = $propertyMetadataFactory;$this->operationPathResolver = $operationPathResolver;$this->oauthEnabled = $oauthEnabled;$this->oauthType = $oauthType;$this->oauthFlow = $oauthFlow;$this->oauthTokenUrl = $oauthTokenUrl;$this->oauthAuthorizationUrl = $oauthAuthorizationUrl;$this->oauthScopes = $oauthScopes;$this->subresourceOperationFactory = $subresourceOperationFactory;$this->paginationEnabled = $paginationEnabled;$this->paginationPageParameterName = $paginationPageParameterName;$this->apiKeys = $apiKeys;$this->clientItemsPerPage = $clientItemsPerPage;$this->itemsPerPageParameterName = $itemsPerPageParameterName;$this->paginationClientEnabled = $paginationClientEnabled;$this->paginationClientEnabledParameterName = $paginationClientEnabledParameterName;$this->defaultContext[self::SPEC_VERSION] = $swaggerVersions[0] ?? 2;$this->defaultContext = array_merge($this->defaultContext, $defaultContext);$this->identifiersExtractor = $identifiersExtractor;$this->openApiNormalizer = $openApiNormalizer;$this->legacyMode = $legacyMode;}/*** {@inheritdoc}** @return array|string|int|float|bool|\ArrayObject|null*/public function normalize($object, $format = null, array $context = []){if ($object instanceof OpenApi) {@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);return $this->openApiNormalizer->normalize($object, $format, $context);}$v3 = 3 === ($context['spec_version'] ?? $this->defaultContext['spec_version']) && !($context['api_gateway'] ?? $this->defaultContext['api_gateway']);$definitions = new \ArrayObject();$paths = new \ArrayObject();$links = new \ArrayObject();if ($this->resourceMetadataFactory instanceof ResourceMetadataCollectionFactoryInterface) {foreach ($object->getResourceNameCollection() as $resourceClass) {$resourceMetadataCollection = $this->resourceMetadataFactory->create($resourceClass);foreach ($resourceMetadataCollection as $i => $resourceMetadata) {$resourceMetadata = $this->transformResourceToResourceMetadata($resourceMetadata);// Items needs to be parsed first to be able to reference the lines from the collection operation$this->addPaths($v3, $paths, $definitions, $resourceClass, $resourceMetadata->getShortName(), $resourceMetadata, OperationType::ITEM, $links);$this->addPaths($v3, $paths, $definitions, $resourceClass, $resourceMetadata->getShortName(), $resourceMetadata, OperationType::COLLECTION, $links);}}$definitions->ksort();$paths->ksort();return $this->computeDoc($v3, $object, $definitions, $paths, $context);}foreach ($object->getResourceNameCollection() as $resourceClass) {$resourceMetadata = $this->resourceMetadataFactory->create($resourceClass);if ($this->identifiersExtractor) {$identifiers = [];if ($resourceMetadata->getItemOperations()) {$identifiers = $this->identifiersExtractor->getIdentifiersFromResourceClass($resourceClass);}$resourceMetadata = $resourceMetadata->withAttributes(($resourceMetadata->getAttributes() ?: []) + ['identifiers' => $identifiers]);}$resourceShortName = $resourceMetadata->getShortName();// Items needs to be parsed first to be able to reference the lines from the collection operation$this->addPaths($v3, $paths, $definitions, $resourceClass, $resourceShortName, $resourceMetadata, OperationType::ITEM, $links);$this->addPaths($v3, $paths, $definitions, $resourceClass, $resourceShortName, $resourceMetadata, OperationType::COLLECTION, $links);if (null === $this->subresourceOperationFactory) {continue;}foreach ($this->subresourceOperationFactory->create($resourceClass) as $operationId => $subresourceOperation) {$method = $resourceMetadata->getTypedOperationAttribute(OperationType::SUBRESOURCE, $subresourceOperation['operation_name'], 'method', 'GET');$paths[$this->getPath($subresourceOperation['shortNames'][0], $subresourceOperation['route_name'], $subresourceOperation, OperationType::SUBRESOURCE)][strtolower($method)] = $this->addSubresourceOperation($v3, $subresourceOperation, $definitions, $operationId, $resourceMetadata);}}$definitions->ksort();$paths->ksort();return $this->computeDoc($v3, $object, $definitions, $paths, $context);}/*** Updates the list of entries in the paths collection.*/private function addPaths(bool $v3, \ArrayObject $paths, \ArrayObject $definitions, string $resourceClass, string $resourceShortName, ResourceMetadata $resourceMetadata, string $operationType, \ArrayObject $links){if (null === $operations = OperationType::COLLECTION === $operationType ? $resourceMetadata->getCollectionOperations() : $resourceMetadata->getItemOperations()) {return;}foreach ($operations as $operationName => $operation) {if (false === ($operation['openapi'] ?? null)) {continue;}// Skolem IRIif ('api_genid' === ($operation['route_name'] ?? null)) {continue;}if (isset($operation['uri_template'])) {$path = str_replace('.{_format}', '', $operation['uri_template']);if (0 !== strpos($path, '/')) {$path = '/'.$path;}} else {$path = $this->getPath($resourceShortName, $operationName, $operation, $operationType);}if ($this->operationMethodResolver) {$method = OperationType::ITEM === $operationType ? $this->operationMethodResolver->getItemOperationMethod($resourceClass, $operationName) : $this->operationMethodResolver->getCollectionOperationMethod($resourceClass, $operationName);} else {$method = $resourceMetadata->getTypedOperationAttribute($operationType, $operationName, 'method', 'GET');}$paths[$path][strtolower($method)] = $this->getPathOperation($v3, $operationName, $operation, $method, $operationType, $resourceClass, $resourceMetadata, $definitions, $links);}}/*** Gets the path for an operation.** If the path ends with the optional _format parameter, it is removed* as optional path parameters are not yet supported.** @see https://github.com/OAI/OpenAPI-Specification/issues/93*/private function getPath(string $resourceShortName, string $operationName, array $operation, string $operationType): string{$path = $this->operationPathResolver->resolveOperationPath($resourceShortName, $operation, $operationType, $operationName);if ('.{_format}' === substr($path, -10)) {$path = substr($path, 0, -10);}return $path;}/*** Gets a path Operation Object.** @see https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#operation-object*/private function getPathOperation(bool $v3, string $operationName, array $operation, string $method, string $operationType, string $resourceClass, ResourceMetadata $resourceMetadata, \ArrayObject $definitions, \ArrayObject $links): \ArrayObject{$pathOperation = new \ArrayObject($operation[$v3 ? 'openapi_context' : 'swagger_context'] ?? []);$resourceShortName = $resourceMetadata->getShortName();$pathOperation['tags'] ?? $pathOperation['tags'] = [$resourceShortName];$pathOperation['operationId'] ?? $pathOperation['operationId'] = lcfirst($operationName).ucfirst($resourceShortName).ucfirst($operationType);if ($v3 && 'GET' === $method && OperationType::ITEM === $operationType && $link = $this->getLinkObject($resourceClass, $pathOperation['operationId'], $this->getPath($resourceShortName, $operationName, $operation, $operationType))) {$links[$pathOperation['operationId']] = $link;}if ($resourceMetadata->getTypedOperationAttribute($operationType, $operationName, 'deprecation_reason', null, true)) {$pathOperation['deprecated'] = true;}if (null === $this->formatsProvider) {$requestFormats = $resourceMetadata->getTypedOperationAttribute($operationType, $operationName, 'input_formats', [], true);$responseFormats = $resourceMetadata->getTypedOperationAttribute($operationType, $operationName, 'output_formats', [], true);} else {$requestFormats = $responseFormats = $this->formatsProvider->getFormatsFromOperation($resourceClass, $operationName, $operationType);}$requestMimeTypes = $this->flattenMimeTypes($requestFormats);$responseMimeTypes = $this->flattenMimeTypes($responseFormats);switch ($method) {case 'GET':return $this->updateGetOperation($v3, $pathOperation, $responseMimeTypes, $operationType, $resourceMetadata, $resourceClass, $resourceShortName, $operationName, $definitions);case 'POST':return $this->updatePostOperation($v3, $pathOperation, $requestMimeTypes, $responseMimeTypes, $operationType, $resourceMetadata, $resourceClass, $resourceShortName, $operationName, $definitions, $links);case 'PATCH':$pathOperation['summary'] ?? $pathOperation['summary'] = sprintf('Updates the %s resource.', $resourceShortName);// no breakcase 'PUT':return $this->updatePutOperation($v3, $pathOperation, $requestMimeTypes, $responseMimeTypes, $operationType, $resourceMetadata, $resourceClass, $resourceShortName, $operationName, $definitions);case 'DELETE':return $this->updateDeleteOperation($v3, $pathOperation, $resourceShortName, $operationType, $operationName, $resourceMetadata, $resourceClass);}return $pathOperation;}/*** @return array the update message as first value, and if the schema is defined as second*/private function addSchemas(bool $v3, array $message, \ArrayObject $definitions, string $resourceClass, string $operationType, string $operationName, array $mimeTypes, string $type = Schema::TYPE_OUTPUT, bool $forceCollection = false): array{if (!$v3) {$jsonSchema = $this->getJsonSchema($v3, $definitions, $resourceClass, $type, $operationType, $operationName, 'json', null, $forceCollection);if (!$jsonSchema->isDefined()) {return [$message, false];}$message['schema'] = $jsonSchema->getArrayCopy(false);return [$message, true];}foreach ($mimeTypes as $mimeType => $format) {$jsonSchema = $this->getJsonSchema($v3, $definitions, $resourceClass, $type, $operationType, $operationName, $format, null, $forceCollection);if (!$jsonSchema->isDefined()) {return [$message, false];}$message['content'][$mimeType] = ['schema' => $jsonSchema->getArrayCopy(false)];}return [$message, true];}private function updateGetOperation(bool $v3, \ArrayObject $pathOperation, array $mimeTypes, string $operationType, ResourceMetadata $resourceMetadata, string $resourceClass, string $resourceShortName, string $operationName, \ArrayObject $definitions): \ArrayObject{$successStatus = (string) $resourceMetadata->getTypedOperationAttribute($operationType, $operationName, 'status', '200');if (!$v3) {$pathOperation['produces'] ?? $pathOperation['produces'] = array_keys($mimeTypes);}if (OperationType::COLLECTION === $operationType) {$outputResourseShortName = $resourceMetadata->getCollectionOperations()[$operationName]['output']['name'] ?? $resourceShortName;$pathOperation['summary'] ?? $pathOperation['summary'] = sprintf('Retrieves the collection of %s resources.', $outputResourseShortName);$successResponse = ['description' => sprintf('%s collection response', $outputResourseShortName)];[$successResponse] = $this->addSchemas($v3, $successResponse, $definitions, $resourceClass, $operationType, $operationName, $mimeTypes);$pathOperation['responses'] ?? $pathOperation['responses'] = [$successStatus => $successResponse];if (($resourceMetadata->getAttributes()['extra_properties']['is_legacy_subresource'] ?? false) ||($resourceMetadata->getAttributes()['extra_properties']['is_alternate_resource_metadata'] ?? false)) {// Avoid duplicates parameters when there is a filter on a subresource identifier$parametersMemory = [];$pathOperation['parameters'] = [];foreach ($resourceMetadata->getCollectionOperations()[$operationName]['identifiers'] as $parameterName => [$class, $identifier]) {$parameter = ['name' => $parameterName, 'in' => 'path', 'required' => true];$v3 ? $parameter['schema'] = ['type' => 'string'] : $parameter['type'] = 'string';$pathOperation['parameters'][] = $parameter;$parametersMemory[] = $parameterName;}if ($parameters = $this->getFiltersParameters($v3, $resourceClass, $operationName, $resourceMetadata)) {foreach ($parameters as $parameter) {if (!\in_array($parameter['name'], $parametersMemory, true)) {$pathOperation['parameters'][] = $parameter;}}}} else {$pathOperation['parameters'] ?? $pathOperation['parameters'] = $this->getFiltersParameters($v3, $resourceClass, $operationName, $resourceMetadata);}$this->addPaginationParameters($v3, $resourceMetadata, OperationType::COLLECTION, $operationName, $pathOperation);return $pathOperation;}$outputResourseShortName = $resourceMetadata->getItemOperations()[$operationName]['output']['name'] ?? $resourceShortName;$pathOperation['summary'] ?? $pathOperation['summary'] = sprintf('Retrieves a %s resource.', $outputResourseShortName);$pathOperation = $this->addItemOperationParameters($v3, $pathOperation, $operationType, $operationName, $resourceMetadata, $resourceClass);$successResponse = ['description' => sprintf('%s resource response', $outputResourseShortName)];[$successResponse] = $this->addSchemas($v3, $successResponse, $definitions, $resourceClass, $operationType, $operationName, $mimeTypes);$pathOperation['responses'] ?? $pathOperation['responses'] = [$successStatus => $successResponse,'404' => ['description' => 'Resource not found'],];return $pathOperation;}private function addPaginationParameters(bool $v3, ResourceMetadata $resourceMetadata, string $operationType, string $operationName, \ArrayObject $pathOperation){if ($this->paginationEnabled && $resourceMetadata->getTypedOperationAttribute($operationType, $operationName, 'pagination_enabled', true, true)) {$paginationParameter = ['name' => $this->paginationPageParameterName,'in' => 'query','required' => false,'description' => 'The collection page number',];$v3 ? $paginationParameter['schema'] = ['type' => 'integer','default' => 1,] : $paginationParameter['type'] = 'integer';$pathOperation['parameters'][] = $paginationParameter;if ($resourceMetadata->getTypedOperationAttribute($operationType, $operationName, 'pagination_client_items_per_page', $this->clientItemsPerPage, true)) {$itemPerPageParameter = ['name' => $this->itemsPerPageParameterName,'in' => 'query','required' => false,'description' => 'The number of items per page',];if ($v3) {$itemPerPageParameter['schema'] = ['type' => 'integer','default' => $resourceMetadata->getTypedOperationAttribute($operationType, $operationName, 'pagination_items_per_page', 30, true),'minimum' => 0,];$maxItemsPerPage = $resourceMetadata->getTypedOperationAttribute($operationType, $operationName, 'maximum_items_per_page', null, true);if (null !== $maxItemsPerPage) {@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);}$maxItemsPerPage = $resourceMetadata->getTypedOperationAttribute($operationType, $operationName, 'pagination_maximum_items_per_page', $maxItemsPerPage, true);if (null !== $maxItemsPerPage) {$itemPerPageParameter['schema']['maximum'] = $maxItemsPerPage;}} else {$itemPerPageParameter['type'] = 'integer';}$pathOperation['parameters'][] = $itemPerPageParameter;}}if ($this->paginationEnabled && $resourceMetadata->getTypedOperationAttribute($operationType, $operationName, 'pagination_client_enabled', $this->paginationClientEnabled, true)) {$paginationEnabledParameter = ['name' => $this->paginationClientEnabledParameterName,'in' => 'query','required' => false,'description' => 'Enable or disable pagination',];$v3 ? $paginationEnabledParameter['schema'] = ['type' => 'boolean'] : $paginationEnabledParameter['type'] = 'boolean';$pathOperation['parameters'][] = $paginationEnabledParameter;}}/*** @throws ResourceClassNotFoundException*/private function addSubresourceOperation(bool $v3, array $subresourceOperation, \ArrayObject $definitions, string $operationId, ResourceMetadata $resourceMetadata): \ArrayObject{$operationName = 'get'; // TODO: we might want to extract that at some point to also support other subresource operations$collection = $subresourceOperation['collection'] ?? false;$subResourceMetadata = $this->resourceMetadataFactory->create($subresourceOperation['resource_class']);$pathOperation = new \ArrayObject([]);$pathOperation['tags'] = $subresourceOperation['shortNames'];$pathOperation['operationId'] = $operationId;$pathOperation['summary'] = sprintf('Retrieves %s%s resource%s.', $subresourceOperation['collection'] ? 'the collection of ' : 'a ', $subresourceOperation['shortNames'][0], $subresourceOperation['collection'] ? 's' : '');if (null === $this->formatsProvider) {// TODO: Subresource operation metadata aren't available by default, for now we have to fallback on default formats.// TODO: A better approach would be to always populate the subresource operation array.$subResourceMetadata = $this->resourceMetadataFactory->create($subresourceOperation['resource_class']);if ($this->resourceMetadataFactory instanceof ResourceMetadataCollectionFactoryInterface) {$subResourceMetadata = $this->transformResourceToResourceMetadata($subResourceMetadata[0]);}$responseFormats = $subResourceMetadata->getTypedOperationAttribute(OperationType::SUBRESOURCE, $operationName, 'output_formats', $this->formats, true);} else {$responseFormats = $this->formatsProvider->getFormatsFromOperation($subresourceOperation['resource_class'], $operationName, OperationType::SUBRESOURCE);}$mimeTypes = $this->flattenMimeTypes($responseFormats);if (!$v3) {$pathOperation['produces'] = array_keys($mimeTypes);}$successResponse = ['description' => sprintf('%s %s response', $subresourceOperation['shortNames'][0], $collection ? 'collection' : 'resource'),];[$successResponse] = $this->addSchemas($v3, $successResponse, $definitions, $subresourceOperation['resource_class'], OperationType::SUBRESOURCE, $operationName, $mimeTypes, Schema::TYPE_OUTPUT, $collection);$pathOperation['responses'] = ['200' => $successResponse, '404' => ['description' => 'Resource not found']];// Avoid duplicates parameters when there is a filter on a subresource identifier$parametersMemory = [];$pathOperation['parameters'] = [];foreach ($subresourceOperation['identifiers'] as $parameterName => [$class, $identifier, $hasIdentifier]) {if (false === strpos($subresourceOperation['path'], sprintf('{%s}', $parameterName))) {continue;}$parameter = ['name' => $parameterName, 'in' => 'path', 'required' => true];$v3 ? $parameter['schema'] = ['type' => 'string'] : $parameter['type'] = 'string';$pathOperation['parameters'][] = $parameter;$parametersMemory[] = $parameterName;}if ($parameters = $this->getFiltersParameters($v3, $subresourceOperation['resource_class'], $operationName, $subResourceMetadata)) {foreach ($parameters as $parameter) {if (!\in_array($parameter['name'], $parametersMemory, true)) {$pathOperation['parameters'][] = $parameter;}}}if ($subresourceOperation['collection']) {$this->addPaginationParameters($v3, $subResourceMetadata, OperationType::SUBRESOURCE, $subresourceOperation['operation_name'], $pathOperation);}return $pathOperation;}private function updatePostOperation(bool $v3, \ArrayObject $pathOperation, array $requestMimeTypes, array $responseMimeTypes, string $operationType, ResourceMetadata $resourceMetadata, string $resourceClass, string $resourceShortName, string $operationName, \ArrayObject $definitions, \ArrayObject $links): \ArrayObject{if (!$v3) {$pathOperation['consumes'] ?? $pathOperation['consumes'] = array_keys($requestMimeTypes);$pathOperation['produces'] ?? $pathOperation['produces'] = array_keys($responseMimeTypes);}$pathOperation['summary'] ?? $pathOperation['summary'] = sprintf('Creates a %s resource.', $resourceShortName);$identifiers = (array) $resourceMetadata->getTypedOperationAttribute($operationType, $operationName, 'identifiers', [], false);$pathOperation = $this->addItemOperationParameters($v3, $pathOperation, $operationType, $operationName, $resourceMetadata, $resourceClass, OperationType::ITEM === $operationType ? false : true);$successResponse = ['description' => sprintf('%s resource created', $resourceShortName)];[$successResponse, $defined] = $this->addSchemas($v3, $successResponse, $definitions, $resourceClass, $operationType, $operationName, $responseMimeTypes);if ($defined && $v3 && ($links[$key = 'get'.ucfirst($resourceShortName).ucfirst(OperationType::ITEM)] ?? null)) {$successResponse['links'] = [ucfirst($key) => $links[$key]];}$pathOperation['responses'] ?? $pathOperation['responses'] = [(string) $resourceMetadata->getTypedOperationAttribute($operationType, $operationName, 'status', '201') => $successResponse,'400' => ['description' => 'Invalid input'],'404' => ['description' => 'Resource not found'],'422' => ['description' => 'Unprocessable entity'],];return $this->addRequestBody($v3, $pathOperation, $definitions, $resourceClass, $resourceShortName, $operationType, $operationName, $requestMimeTypes);}private function updatePutOperation(bool $v3, \ArrayObject $pathOperation, array $requestMimeTypes, array $responseMimeTypes, string $operationType, ResourceMetadata $resourceMetadata, string $resourceClass, string $resourceShortName, string $operationName, \ArrayObject $definitions): \ArrayObject{if (!$v3) {$pathOperation['consumes'] ?? $pathOperation['consumes'] = array_keys($requestMimeTypes);$pathOperation['produces'] ?? $pathOperation['produces'] = array_keys($responseMimeTypes);}$pathOperation['summary'] ?? $pathOperation['summary'] = sprintf('Replaces the %s resource.', $resourceShortName);$pathOperation = $this->addItemOperationParameters($v3, $pathOperation, $operationType, $operationName, $resourceMetadata, $resourceClass);$successResponse = ['description' => sprintf('%s resource updated', $resourceShortName)];[$successResponse] = $this->addSchemas($v3, $successResponse, $definitions, $resourceClass, $operationType, $operationName, $responseMimeTypes);$pathOperation['responses'] ?? $pathOperation['responses'] = [(string) $resourceMetadata->getTypedOperationAttribute($operationType, $operationName, 'status', '200') => $successResponse,'400' => ['description' => 'Invalid input'],'404' => ['description' => 'Resource not found'],'422' => ['description' => 'Unprocessable entity'],];return $this->addRequestBody($v3, $pathOperation, $definitions, $resourceClass, $resourceShortName, $operationType, $operationName, $requestMimeTypes, true);}private function addRequestBody(bool $v3, \ArrayObject $pathOperation, \ArrayObject $definitions, string $resourceClass, string $resourceShortName, string $operationType, string $operationName, array $requestMimeTypes, bool $put = false){if (isset($pathOperation['requestBody'])) {return $pathOperation;}[$message, $defined] = $this->addSchemas($v3, [], $definitions, $resourceClass, $operationType, $operationName, $requestMimeTypes, Schema::TYPE_INPUT);if (!$defined) {return $pathOperation;}$description = sprintf('The %s %s resource', $put ? 'updated' : 'new', $resourceShortName);if ($v3) {$pathOperation['requestBody'] = $message + ['description' => $description];return $pathOperation;}if (!$this->hasBodyParameter($pathOperation['parameters'] ?? [])) {$pathOperation['parameters'][] = ['name' => lcfirst($resourceShortName),'in' => 'body','description' => $description,] + $message;}return $pathOperation;}private function hasBodyParameter(array $parameters): bool{foreach ($parameters as $parameter) {if (\array_key_exists('in', $parameter) && 'body' === $parameter['in']) {return true;}}return false;}private function updateDeleteOperation(bool $v3, \ArrayObject $pathOperation, string $resourceShortName, string $operationType, string $operationName, ResourceMetadata $resourceMetadata, string $resourceClass): \ArrayObject{$pathOperation['summary'] ?? $pathOperation['summary'] = sprintf('Removes the %s resource.', $resourceShortName);$pathOperation['responses'] ?? $pathOperation['responses'] = [(string) $resourceMetadata->getTypedOperationAttribute($operationType, $operationName, 'status', '204') => ['description' => sprintf('%s resource deleted', $resourceShortName)],'404' => ['description' => 'Resource not found'],];return $this->addItemOperationParameters($v3, $pathOperation, $operationType, $operationName, $resourceMetadata, $resourceClass);}private function addItemOperationParameters(bool $v3, \ArrayObject $pathOperation, string $operationType, string $operationName, ResourceMetadata $resourceMetadata, string $resourceClass, bool $isPost = false): \ArrayObject{$identifiers = (array) $resourceMetadata->getTypedOperationAttribute($operationType, $operationName, 'identifiers', [], false);// Auto-generated routes in API Platform < 2.7 are considered as collection, hotfix this as the OpenApi Factory supports new operations anyways.// this also fixes a bug where we could not create POST item operations in API P 2.6if (OperationType::ITEM === $operationType && $isPost) {$operationType = OperationType::COLLECTION;}if (!$identifiers && OperationType::COLLECTION !== $operationType) {try {$identifiers = $this->identifiersExtractor->getIdentifiersFromResourceClass($resourceClass);} catch (RuntimeException $e) {// Ignore exception here} catch (ResourceClassNotFoundException $e) {if (false === $this->legacyMode) {// Skipping these, swagger is not compatible with post 2.7 resource metadatareturn $pathOperation;}throw $e;}}if (\count($identifiers) > 1 ? $resourceMetadata->getItemOperationAttribute($operationName, 'composite_identifier', true, true) : false) {$identifiers = ['id'];}if (!$identifiers && OperationType::COLLECTION === $operationType) {return $pathOperation;}if (!isset($pathOperation['parameters'])) {$pathOperation['parameters'] = [];}foreach ($identifiers as $parameterName => $identifier) {$parameter = ['name' => \is_string($parameterName) ? $parameterName : $identifier,'in' => 'path','required' => true,];$v3 ? $parameter['schema'] = ['type' => 'string'] : $parameter['type'] = 'string';$pathOperation['parameters'][] = $parameter;}return $pathOperation;}private function getJsonSchema(bool $v3, \ArrayObject $definitions, string $resourceClass, string $type, ?string $operationType, ?string $operationName, string $format = 'json', ?array $serializerContext = null, bool $forceCollection = false): Schema{$schema = new Schema($v3 ? Schema::VERSION_OPENAPI : Schema::VERSION_SWAGGER);$schema->setDefinitions($definitions);if ($this->jsonSchemaFactory instanceof SchemaFactoryInterface) {$operation = $operationName ? (new class() extends HttpOperation {})->withName($operationName) : null;return $this->jsonSchemaFactory->buildSchema($resourceClass, $format, $type, $operation, $schema, $serializerContext, $forceCollection);}return $this->jsonSchemaFactory->buildSchema($resourceClass, $format, $type, $operationType, $operationName, $schema, $serializerContext, $forceCollection);}private function computeDoc(bool $v3, Documentation $documentation, \ArrayObject $definitions, \ArrayObject $paths, array $context): array{$baseUrl = $context[self::BASE_URL] ?? $this->defaultContext[self::BASE_URL];if ($v3) {$docs = ['openapi' => self::OPENAPI_VERSION];if ('/' !== $baseUrl && '' !== $baseUrl) {$docs['servers'] = [['url' => $baseUrl]];}} else {$docs = ['swagger' => self::SWAGGER_VERSION,'basePath' => $baseUrl,];}$docs += ['info' => ['title' => $documentation->getTitle(),'version' => $documentation->getVersion(),],'paths' => $paths,];if ('' !== $description = $documentation->getDescription()) {$docs['info']['description'] = $description;}$securityDefinitions = [];$security = [];if ($this->oauthEnabled) {$oauthAttributes = ['authorizationUrl' => $this->oauthAuthorizationUrl,'scopes' => new \ArrayObject($this->oauthScopes),];if ($this->oauthTokenUrl) {$oauthAttributes['tokenUrl'] = $this->oauthTokenUrl;}$securityDefinitions['oauth'] = ['type' => $this->oauthType,'description' => sprintf('OAuth 2.0 %s Grant',strtolower(preg_replace('/[A-Z]/', ' \\0', lcfirst($this->oauthFlow)))),];if ($v3) {$securityDefinitions['oauth']['flows'] = [$this->oauthFlow => $oauthAttributes,];} else {$securityDefinitions['oauth']['flow'] = $this->oauthFlow;$securityDefinitions['oauth'] = array_merge($securityDefinitions['oauth'], $oauthAttributes);}$security[] = ['oauth' => []];}foreach ($this->apiKeys as $key => $apiKey) {$name = $apiKey['name'];$type = $apiKey['type'];$securityDefinitions[$key] = ['type' => 'apiKey','in' => $type,'description' => sprintf('Value for the %s %s', $name, 'query' === $type ? sprintf('%s parameter', $type) : $type),'name' => $name,];$security[] = [$key => []];}if ($securityDefinitions && $security) { // @phpstan-ignore-line false positive$docs['security'] = $security;if (!$v3) {$docs['securityDefinitions'] = $securityDefinitions;}}if ($v3) {if (\count($definitions) + \count($securityDefinitions)) {$docs['components'] = [];if (\count($definitions)) {$docs['components']['schemas'] = $definitions;}if (\count($securityDefinitions)) {$docs['components']['securitySchemes'] = $securityDefinitions;}}} elseif (\count($definitions) > 0) {$docs['definitions'] = $definitions;}return $docs;}/*** Gets parameters corresponding to enabled filters.*/private function getFiltersParameters(bool $v3, string $resourceClass, string $operationName, ResourceMetadata $resourceMetadata): array{if (null === $this->filterLocator) {return [];}$parameters = [];$resourceFilters = $resourceMetadata->getCollectionOperationAttribute($operationName, 'filters', [], true);foreach ($resourceFilters as $filterId) {if (!$filter = $this->getFilter($filterId)) {continue;}foreach ($filter->getDescription($resourceClass) as $name => $data) {$parameter = ['name' => $name,'in' => 'query','required' => $data['required'],];$type = \in_array($data['type'], Type::$builtinTypes, true) ? $this->jsonSchemaTypeFactory->getType(new Type($data['type'], false, null, $data['is_collection'] ?? false)) : ['type' => 'string'];$v3 ? $parameter['schema'] = $type : $parameter += $type;if ($v3 && isset($data['schema'])) {$parameter['schema'] = $data['schema'];}if ('array' === ($type['type'] ?? '')) {$deepObject = \in_array($data['type'], [Type::BUILTIN_TYPE_ARRAY, Type::BUILTIN_TYPE_OBJECT], true);if ($v3) {$parameter['style'] = $deepObject ? 'deepObject' : 'form';$parameter['explode'] = true;} else {$parameter['collectionFormat'] = $deepObject ? 'csv' : 'multi';}}$key = $v3 ? 'openapi' : 'swagger';if (isset($data[$key])) {$parameter = $data[$key] + $parameter;}$parameters[] = $parameter;}}return $parameters;}/*** {@inheritdoc}*/public function supportsNormalization($data, $format = null, array $context = []): bool{return self::FORMAT === $format && ($data instanceof Documentation || $this->openApiNormalizer && $data instanceof OpenApi);}/*** {@inheritdoc}*/public function hasCacheableSupportsMethod(): bool{return true;}private function flattenMimeTypes(array $responseFormats): array{$responseMimeTypes = [];foreach ($responseFormats as $responseFormat => $mimeTypes) {foreach ($mimeTypes as $mimeType) {$responseMimeTypes[$mimeType] = $responseFormat;}}return $responseMimeTypes;}/*** https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.0.md#linkObject.*/private function getLinkObject(string $resourceClass, string $operationId, string $path): array{$linkObject = $identifiers = [];foreach ($this->propertyNameCollectionFactory->create($resourceClass) as $propertyName) {$propertyMetadata = $this->propertyMetadataFactory->create($resourceClass, $propertyName);if (!$propertyMetadata->isIdentifier()) {continue;}$linkObject['parameters'][$propertyName] = sprintf('$response.body#/%s', $propertyName);$identifiers[] = $propertyName;}if (!$linkObject) {return [];}$linkObject['operationId'] = $operationId;$linkObject['description'] = 1 === \count($identifiers) ? sprintf('The `%1$s` value returned in the response can be used as the `%1$s` parameter in `GET %2$s`.', $identifiers[0], $path) : sprintf('The values returned in the response can be used in `GET %s`.', $path);return $linkObject;}}