diff --git a/src/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php b/src/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php index b6b84eb7387..bddb5b9455f 100644 --- a/src/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php +++ b/src/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php @@ -392,6 +392,10 @@ private function registerSwaggerConfiguration(ContainerBuilder $container, array $container->setParameter('api_platform.enable_swagger_ui', $config['enable_swagger_ui']); $container->setParameter('api_platform.enable_re_doc', $config['enable_re_doc']); $container->setParameter('api_platform.swagger.api_keys', $config['swagger']['api_keys']); + + if (true === $config['openapi']['backward_compatibility_layer']) { + $container->getDefinition('api_platform.swagger.normalizer.documentation')->addArgument($container->getDefinition('api_platform.openapi.normalizer')); + } } private function registerJsonApiConfiguration(array $formats, XmlFileLoader $loader): void diff --git a/src/Bridge/Symfony/Bundle/DependencyInjection/Configuration.php b/src/Bridge/Symfony/Bundle/DependencyInjection/Configuration.php index b0dfe68cf3e..de9a01c80a8 100644 --- a/src/Bridge/Symfony/Bundle/DependencyInjection/Configuration.php +++ b/src/Bridge/Symfony/Bundle/DependencyInjection/Configuration.php @@ -483,6 +483,7 @@ private function addOpenApiSection(ArrayNodeDefinition $rootNode): void ->scalarNode('email')->defaultNull()->info('The email address of the contact person/organization. MUST be in the format of an email address.')->end() ->end() ->end() + ->booleanNode('backward_compatibility_layer')->defaultTrue()->info('Enable this to decorate the "api_platform.swagger.normalizer.documentation" instead of decorating the OpenAPI factory.')->end() ->scalarNode('termsOfService')->defaultNull()->info('A URL to the Terms of Service for the API. MUST be in the format of a URL.')->end() ->arrayNode('license') ->addDefaultsIfNotSet() diff --git a/src/Bridge/Symfony/Bundle/Resources/config/openapi.xml b/src/Bridge/Symfony/Bundle/Resources/config/openapi.xml index f512cfd7f0b..8d2fc23d4ab 100644 --- a/src/Bridge/Symfony/Bundle/Resources/config/openapi.xml +++ b/src/Bridge/Symfony/Bundle/Resources/config/openapi.xml @@ -7,7 +7,8 @@ - + + diff --git a/src/OpenApi/Serializer/OpenApiNormalizer.php b/src/OpenApi/Serializer/OpenApiNormalizer.php index 7445f6a75c0..8b4148cf472 100644 --- a/src/OpenApi/Serializer/OpenApiNormalizer.php +++ b/src/OpenApi/Serializer/OpenApiNormalizer.php @@ -55,13 +55,17 @@ private function recursiveClean($data): array } if ('schemas' === $key) { - ksort($value); + if ($value) { + ksort($value); + } } // Side effect of using getPaths(): Paths which itself contains the array if ('paths' === $key) { $value = $data['paths'] = $data['paths']['paths']; - ksort($value); + if ($value) { + ksort($value); + } unset($data['paths']['paths']); } diff --git a/src/Swagger/Serializer/DocumentationNormalizer.php b/src/Swagger/Serializer/DocumentationNormalizer.php index 33d5fa23f1c..62aba70ec07 100644 --- a/src/Swagger/Serializer/DocumentationNormalizer.php +++ b/src/Swagger/Serializer/DocumentationNormalizer.php @@ -33,6 +33,7 @@ use ApiPlatform\Core\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface; use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface; use ApiPlatform\Core\Metadata\Resource\ResourceMetadata; +use ApiPlatform\Core\OpenApi\OpenApi; use ApiPlatform\Core\Operation\Factory\SubresourceOperationFactoryInterface; use ApiPlatform\Core\PathResolver\OperationPathResolverInterface; use Psr\Container\ContainerInterface; @@ -102,6 +103,8 @@ final class DocumentationNormalizer implements NormalizerInterface, CacheableSup private $identifiersExtractor; + private $openApiNormalizer; + /** * @param SchemaFactoryInterface|ResourceClassResolverInterface|null $jsonSchemaFactory * @param ContainerInterface|FilterCollection|null $filterLocator @@ -109,7 +112,7 @@ final class DocumentationNormalizer implements NormalizerInterface, CacheableSup * @param mixed|null $jsonSchemaTypeFactory * @param int[] $swaggerVersions */ - public function __construct(ResourceMetadataFactoryInterface $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) + public function __construct(ResourceMetadataFactoryInterface $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) { 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); @@ -171,6 +174,7 @@ public function __construct(ResourceMetadataFactoryInterface $resourceMetadataFa $this->defaultContext = array_merge($this->defaultContext, $defaultContext); $this->identifiersExtractor = $identifiersExtractor; + $this->openApiNormalizer = $openApiNormalizer; } /** @@ -178,6 +182,12 @@ public function __construct(ResourceMetadataFactoryInterface $resourceMetadataFa */ 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(); @@ -779,7 +789,7 @@ private function getFiltersParameters(bool $v3, string $resourceClass, string $o */ public function supportsNormalization($data, $format = null): bool { - return self::FORMAT === $format && $data instanceof Documentation; + return self::FORMAT === $format && ($data instanceof Documentation || $this->openApiNormalizer && $data instanceof OpenApi); } /** diff --git a/tests/Bridge/Symfony/Bundle/DependencyInjection/ConfigurationTest.php b/tests/Bridge/Symfony/Bundle/DependencyInjection/ConfigurationTest.php index 737b5df99cf..1df2208bb80 100644 --- a/tests/Bridge/Symfony/Bundle/DependencyInjection/ConfigurationTest.php +++ b/tests/Bridge/Symfony/Bundle/DependencyInjection/ConfigurationTest.php @@ -218,6 +218,7 @@ private function runDefaultConfigTests(array $doctrineIntegrationsToLoad = ['orm 'name' => null, 'url' => null, ], + 'backward_compatibility_layer' => true, ], ], $config); } diff --git a/tests/Fixtures/app/config/config_common.yml b/tests/Fixtures/app/config/config_common.yml index 4bf5207f6b7..060dde81d0c 100644 --- a/tests/Fixtures/app/config/config_common.yml +++ b/tests/Fixtures/app/config/config_common.yml @@ -73,6 +73,8 @@ api_platform: http_cache: invalidation: enabled: true + openapi: + backward_compatibility_layer: false defaults: pagination_client_enabled: true pagination_client_items_per_page: true diff --git a/tests/Swagger/Serializer/DocumentationNormalizerV3Test.php b/tests/Swagger/Serializer/DocumentationNormalizerV3Test.php index 407c28f6c75..6d7de03e455 100644 --- a/tests/Swagger/Serializer/DocumentationNormalizerV3Test.php +++ b/tests/Swagger/Serializer/DocumentationNormalizerV3Test.php @@ -18,6 +18,7 @@ use ApiPlatform\Core\Api\OperationAwareFormatsProviderInterface; use ApiPlatform\Core\Api\OperationMethodResolverInterface; use ApiPlatform\Core\Api\OperationType; +use ApiPlatform\Core\Api\ResourceClassResolverInterface; use ApiPlatform\Core\Bridge\Symfony\Routing\RouterOperationPathResolver; use ApiPlatform\Core\Documentation\Documentation; use ApiPlatform\Core\Exception\InvalidArgumentException; @@ -33,6 +34,8 @@ use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface; use ApiPlatform\Core\Metadata\Resource\ResourceMetadata; use ApiPlatform\Core\Metadata\Resource\ResourceNameCollection; +use ApiPlatform\Core\OpenApi\Model; +use ApiPlatform\Core\OpenApi\OpenApi; use ApiPlatform\Core\Operation\Factory\SubresourceOperationFactory; use ApiPlatform\Core\Operation\UnderscorePathSegmentNameGenerator; use ApiPlatform\Core\PathResolver\CustomOperationPathResolver; @@ -54,6 +57,7 @@ use Symfony\Component\Routing\RouterInterface; use Symfony\Component\Serializer\NameConverter\NameConverterInterface; use Symfony\Component\Serializer\Normalizer\AbstractNormalizer; +use Symfony\Component\Serializer\Normalizer\NormalizerInterface; /** * @author Amrouche Hamza @@ -3208,4 +3212,55 @@ private function doTestNormalizeWithCustomFormatsDefinedAtOperationLevel(Operati $this->assertEquals($expected, $normalizer->normalize($documentation, DocumentationNormalizer::FORMAT, ['base_url' => '/'])); } + + /** + * @group legacy + * @expectedDeprecation Using the swagger DocumentationNormalizer is deprecated in favor of decorating the OpenApiFactory, use the "openapi.backward_compatibility_layer" configuration to change this behavior. + */ + public function testNormalizeOpenApi() + { + $openapi = new OpenApi(new Model\Info('api', 'v1'), [], new Model\Paths()); + $propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class); + $resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataFactoryInterface::class); + $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); + $resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class); + $operationPathResolver = new OperationPathResolver(new UnderscorePathSegmentNameGenerator()); + $identifiersExtractorProphecy = $this->prophesize(IdentifiersExtractorInterface::class); + + $openApiNormalizerProphecy = $this->prophesize(NormalizerInterface::class); + $openApiNormalizerProphecy->normalize($openapi, null, [])->willReturn([])->shouldBeCalled(); + + $normalizer = new DocumentationNormalizer( + $resourceMetadataFactoryProphecy->reveal(), + $propertyNameCollectionFactoryProphecy->reveal(), + $propertyMetadataFactoryProphecy->reveal(), + null, + null, + $operationPathResolver, + null, + null, + null, false, + '', + '', + '', + '', + [], + [], + null, + false, + 'page', + false, + 'itemsPerPage', + $formatsProvider ?? [], + false, + 'pagination', + ['spec_version' => 3], + [2, 3], + $identifiersExtractorProphecy->reveal(), + $openApiNormalizerProphecy->reveal() + ); + + $this->assertTrue($normalizer->supportsNormalization($openapi, 'json')); + $this->assertEquals([], $normalizer->normalize($openapi)); + } }