From 8c513c021114c7e1ee4c6760bb270d2323ed17d2 Mon Sep 17 00:00:00 2001 From: 4lxndr Date: Wed, 17 Dec 2025 21:45:45 +0100 Subject: [PATCH 1/2] fix(openapi): enable ReDoc when Swagger UI is disabled --- src/Symfony/Action/DocumentationAction.php | 7 ++- .../ApiPlatformExtension.php | 2 +- .../Resources/config/symfony/controller.php | 1 + .../Resources/config/symfony/events.php | 1 + .../Tests/Action/DocumentationActionTest.php | 63 ++++++++++++++++++- .../ApiPlatformExtensionTest.php | 21 +++++++ tests/Functional/DocumentationActionTest.php | 52 +++++++++++++-- 7 files changed, 136 insertions(+), 11 deletions(-) diff --git a/src/Symfony/Action/DocumentationAction.php b/src/Symfony/Action/DocumentationAction.php index fd5ea76b2d..07c21353b4 100644 --- a/src/Symfony/Action/DocumentationAction.php +++ b/src/Symfony/Action/DocumentationAction.php @@ -50,6 +50,7 @@ public function __construct( ?Negotiator $negotiator = null, private readonly array $documentationFormats = [OpenApiNormalizer::JSON_FORMAT => ['application/vnd.openapi+json'], OpenApiNormalizer::FORMAT => ['application/json']], private readonly bool $swaggerUiEnabled = true, + private readonly bool $reDocEnabled = true, private readonly bool $docsEnabled = true, ) { $this->negotiator = $negotiator ?? new Negotiator(); @@ -90,8 +91,8 @@ public function __invoke(?Request $request = null) */ private function getOpenApiDocumentation(array $context, string $format, Request $request): OpenApi|Response { - if ('html' === $format && !$this->swaggerUiEnabled) { - throw new NotFoundHttpException('Swagger UI is disabled.'); + if ('html' === $format && !$this->swaggerUiEnabled && !$this->reDocEnabled) { + throw new NotFoundHttpException('Swagger UI and ReDoc are disabled.'); } if ($this->provider && $this->processor) { @@ -104,7 +105,7 @@ class: OpenApi::class, outputFormats: $this->documentationFormats ); - if ('html' === $format && $this->swaggerUiEnabled) { + if ('html' === $format && ($this->swaggerUiEnabled || $this->reDocEnabled)) { $operation = $operation->withProcessor('api_platform.swagger_ui.processor')->withWrite(true); } diff --git a/src/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php b/src/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php index 405af850fc..2d23932693 100644 --- a/src/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php +++ b/src/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php @@ -604,7 +604,7 @@ private function registerSwaggerConfiguration(ContainerBuilder $container, array $loader->load('openapi/yaml.php'); } - if ($config['enable_swagger_ui']) { + if ($config['enable_swagger_ui'] || $config['enable_re_doc']) { $loader->load('swagger_ui.php'); if ($config['use_symfony_listeners']) { diff --git a/src/Symfony/Bundle/Resources/config/symfony/controller.php b/src/Symfony/Bundle/Resources/config/symfony/controller.php index ad1a9a9e50..d97adc6277 100644 --- a/src/Symfony/Bundle/Resources/config/symfony/controller.php +++ b/src/Symfony/Bundle/Resources/config/symfony/controller.php @@ -48,6 +48,7 @@ service('api_platform.negotiator')->nullOnInvalid(), '%api_platform.docs_formats%', '%api_platform.enable_swagger_ui%', + '%api_platform.enable_re_doc%', '%api_platform.enable_docs%', ]); }; diff --git a/src/Symfony/Bundle/Resources/config/symfony/events.php b/src/Symfony/Bundle/Resources/config/symfony/events.php index 32885d4714..9b07ff7dc5 100644 --- a/src/Symfony/Bundle/Resources/config/symfony/events.php +++ b/src/Symfony/Bundle/Resources/config/symfony/events.php @@ -180,6 +180,7 @@ service('api_platform.negotiator')->nullOnInvalid(), '%api_platform.docs_formats%', '%api_platform.enable_swagger_ui%', + '%api_platform.enable_re_doc%', '%api_platform.enable_docs%', ]); diff --git a/src/Symfony/Tests/Action/DocumentationActionTest.php b/src/Symfony/Tests/Action/DocumentationActionTest.php index e98b7658ae..5117c8d868 100644 --- a/src/Symfony/Tests/Action/DocumentationActionTest.php +++ b/src/Symfony/Tests/Action/DocumentationActionTest.php @@ -36,10 +36,10 @@ class DocumentationActionTest extends TestCase { use ProphecyTrait; - public function testHtmlFormatWhenSwaggerUiDisabledThrows404(): void + public function testHtmlFormatWhenSwaggerUiAndReDocDisabledThrows404(): void { $this->expectException(NotFoundHttpException::class); - $this->expectExceptionMessage('Swagger UI is disabled.'); + $this->expectExceptionMessage('Swagger UI and ReDoc are disabled.'); $request = new Request(); $request->attributes->set('_format', 'html'); @@ -55,11 +55,70 @@ public function testHtmlFormatWhenSwaggerUiDisabledThrows404(): void 'html' => ['text/html'], ], swaggerUiEnabled: false, + reDocEnabled: false, ); $documentation($request); } + public function testHtmlFormatWhenReDocEnabledAndSwaggerUiDisabled(): void + { + $request = new Request(); + $request->attributes->set('_format', 'html'); + + $openApiFactory = $this->createMock(OpenApiFactoryInterface::class); + $resourceNameCollectionFactory = $this->createMock(ResourceNameCollectionFactoryInterface::class); + $provider = $this->createMock(ProviderInterface::class); + $provider->expects($this->once())->method('provide')->willReturn(new OpenApi(new Info('title', '1.0.0'), [], new Paths())); + $processor = $this->createMock(ProcessorInterface::class); + $processor->expects($this->once())->method('process')->willReturnArgument(0); + + $documentation = new DocumentationAction( + $resourceNameCollectionFactory, + openApiFactory: $openApiFactory, + provider: $provider, + processor: $processor, + documentationFormats: [ + 'json' => ['application/json'], + 'html' => ['text/html'], + ], + swaggerUiEnabled: false, + reDocEnabled: true, + ); + + $result = $documentation($request); + $this->assertInstanceOf(OpenApi::class, $result); + } + + public function testHtmlFormatWhenSwaggerUiEnabledAndReDocDisabled(): void + { + $request = new Request(); + $request->attributes->set('_format', 'html'); + + $openApiFactory = $this->createMock(OpenApiFactoryInterface::class); + $resourceNameCollectionFactory = $this->createMock(ResourceNameCollectionFactoryInterface::class); + $provider = $this->createMock(ProviderInterface::class); + $provider->expects($this->once())->method('provide')->willReturn(new OpenApi(new Info('title', '1.0.0'), [], new Paths())); + $processor = $this->createMock(ProcessorInterface::class); + $processor->expects($this->once())->method('process')->willReturnArgument(0); + + $documentation = new DocumentationAction( + $resourceNameCollectionFactory, + openApiFactory: $openApiFactory, + provider: $provider, + processor: $processor, + documentationFormats: [ + 'json' => ['application/json'], + 'html' => ['text/html'], + ], + swaggerUiEnabled: true, + reDocEnabled: false, + ); + + $result = $documentation($request); + $this->assertInstanceOf(OpenApi::class, $result); + } + public function testJsonFormatWhenSwaggerUiDisabledIsAccessible(): void { $request = new Request(); diff --git a/src/Symfony/Tests/Bundle/DependencyInjection/ApiPlatformExtensionTest.php b/src/Symfony/Tests/Bundle/DependencyInjection/ApiPlatformExtensionTest.php index 29e184fd13..8cb15c4624 100644 --- a/src/Symfony/Tests/Bundle/DependencyInjection/ApiPlatformExtensionTest.php +++ b/src/Symfony/Tests/Bundle/DependencyInjection/ApiPlatformExtensionTest.php @@ -268,6 +268,7 @@ public function testSwaggerUiDisabledConfiguration(): void $config = self::DEFAULT_CONFIG; $config['api_platform']['enable_swagger'] = true; $config['api_platform']['enable_swagger_ui'] = false; + $config['api_platform']['enable_re_doc'] = false; $config['api_platform']['use_symfony_listeners'] = true; (new ApiPlatformExtension())->load($config, $this->container); @@ -300,6 +301,26 @@ public function testSwaggerUiEnabledConfiguration(): void $this->assertContainerHas($services); } + public function testReDocEnabledWithSwaggerUiDisabledConfiguration(): void + { + $config = self::DEFAULT_CONFIG; + $config['api_platform']['enable_swagger'] = true; + $config['api_platform']['enable_swagger_ui'] = false; + $config['api_platform']['enable_re_doc'] = true; + $config['api_platform']['use_symfony_listeners'] = true; + + (new ApiPlatformExtension())->load($config, $this->container); + + $services = [ + 'api_platform.swagger_ui.processor', + 'api_platform.swagger_ui.context', + 'api_platform.swagger_ui.provider', + 'api_platform.swagger_ui.documentation.provider', + ]; + + $this->assertContainerHas($services); + } + public function testEventListenersConfiguration(): void { $config = self::DEFAULT_CONFIG; diff --git a/tests/Functional/DocumentationActionTest.php b/tests/Functional/DocumentationActionTest.php index 707759866e..74e80a27f1 100644 --- a/tests/Functional/DocumentationActionTest.php +++ b/tests/Functional/DocumentationActionTest.php @@ -23,17 +23,18 @@ class DocumentationActionAppKernel extends \AppKernel { public static bool $swaggerUiEnabled = true; + public static bool $reDocEnabled = true; public function getCacheDir(): string { - $suffix = self::$swaggerUiEnabled ? 'ui_enabled' : 'ui_disabled'; + $suffix = (self::$swaggerUiEnabled ? 'ui_' : 'no_ui_').(self::$reDocEnabled ? 'redoc' : 'no_redoc'); return parent::getCacheDir().'/'.$suffix; } public function getLogDir(): string { - $suffix = self::$swaggerUiEnabled ? 'ui_enabled' : 'ui_disabled'; + $suffix = (self::$swaggerUiEnabled ? 'ui_' : 'no_ui_').(self::$reDocEnabled ? 'redoc' : 'no_redoc'); return parent::getLogDir().'/'.$suffix; } @@ -45,6 +46,7 @@ protected function configureContainer(ContainerBuilder $c, LoaderInterface $load $loader->load(static function (ContainerBuilder $container) { $container->loadFromExtension('api_platform', [ 'enable_swagger_ui' => DocumentationActionAppKernel::$swaggerUiEnabled, + 'enable_re_doc' => DocumentationActionAppKernel::$reDocEnabled, ]); }); } @@ -59,28 +61,32 @@ protected static function getKernelClass(): string return DocumentationActionAppKernel::class; } - public function testHtmlDocumentationIsNotAccessibleWhenSwaggerUiIsDisabled(): void + public function testHtmlDocumentationIsNotAccessibleWhenSwaggerUiAndReDocAreDisabled(): void { DocumentationActionAppKernel::$swaggerUiEnabled = false; + DocumentationActionAppKernel::$reDocEnabled = false; $client = self::createClient(); $container = static::getContainer(); $this->assertFalse($container->getParameter('api_platform.enable_swagger_ui')); + $this->assertFalse($container->getParameter('api_platform.enable_re_doc')); $client->request('GET', '/docs', ['headers' => ['Accept' => 'text/html']]); $this->assertResponseStatusCodeSame(404); - $this->assertStringContainsString('Swagger UI is disabled.', $client->getResponse()->getContent(false)); + $this->assertStringContainsString('Swagger UI and ReDoc are disabled.', $client->getResponse()->getContent(false)); } public function testJsonDocumentationIsAccessibleWhenSwaggerUiIsDisabled(): void { DocumentationActionAppKernel::$swaggerUiEnabled = false; + DocumentationActionAppKernel::$reDocEnabled = false; $client = self::createClient(); $container = static::getContainer(); $this->assertFalse($container->getParameter('api_platform.enable_swagger_ui')); + $this->assertFalse($container->getParameter('api_platform.enable_re_doc')); $client->request('GET', '/docs.jsonopenapi', ['headers' => ['Accept' => 'application/vnd.openapi+json']]); $this->assertResponseIsSuccessful(); @@ -88,28 +94,64 @@ public function testJsonDocumentationIsAccessibleWhenSwaggerUiIsDisabled(): void $this->assertJsonContains(['info' => ['title' => 'My Dummy API']]); } + public function testHtmlDocumentationIsAccessibleWhenReDocEnabledAndSwaggerUiDisabled(): void + { + DocumentationActionAppKernel::$swaggerUiEnabled = false; + DocumentationActionAppKernel::$reDocEnabled = true; + + $client = self::createClient(); + + $container = static::getContainer(); + $this->assertFalse($container->getParameter('api_platform.enable_swagger_ui')); + $this->assertTrue($container->getParameter('api_platform.enable_re_doc')); + + $client->request('GET', '/docs', ['headers' => ['Accept' => 'text/html']]); + $this->assertResponseIsSuccessful(); + $this->assertStringNotContainsString('Swagger UI and ReDoc are disabled.', $client->getResponse()->getContent(false)); + } + + public function testHtmlDocumentationIsAccessibleWhenSwaggerUiEnabledAndReDocDisabled(): void + { + DocumentationActionAppKernel::$swaggerUiEnabled = true; + DocumentationActionAppKernel::$reDocEnabled = false; + + $client = self::createClient(); + + $container = static::getContainer(); + $this->assertTrue($container->getParameter('api_platform.enable_swagger_ui')); + $this->assertFalse($container->getParameter('api_platform.enable_re_doc')); + + $client->request('GET', '/docs', ['headers' => ['Accept' => 'text/html']]); + $this->assertResponseIsSuccessful(); + $this->assertStringNotContainsString('Swagger UI and ReDoc are disabled.', $client->getResponse()->getContent(false)); + } + public function testHtmlDocumentationIsAccessibleWhenSwaggerUiIsEnabled(): void { DocumentationActionAppKernel::$swaggerUiEnabled = true; + DocumentationActionAppKernel::$reDocEnabled = true; $client = self::createClient(); $container = static::getContainer(); $this->assertTrue($container->getParameter('api_platform.enable_swagger_ui')); + $this->assertTrue($container->getParameter('api_platform.enable_re_doc')); $client->request('GET', '/docs', ['headers' => ['Accept' => 'text/html']]); $this->assertResponseIsSuccessful(); - $this->assertStringNotContainsString('Swagger UI is disabled.', $client->getResponse()->getContent(false)); + $this->assertStringNotContainsString('Swagger UI and ReDoc are disabled.', $client->getResponse()->getContent(false)); } public function testJsonDocumentationIsAccessibleWhenSwaggerUiIsEnabled(): void { DocumentationActionAppKernel::$swaggerUiEnabled = true; + DocumentationActionAppKernel::$reDocEnabled = true; $client = self::createClient(); $container = static::getContainer(); $this->assertTrue($container->getParameter('api_platform.enable_swagger_ui')); + $this->assertTrue($container->getParameter('api_platform.enable_re_doc')); $client->request('GET', '/docs.jsonopenapi', ['headers' => ['Accept' => 'application/vnd.openapi+json']]); $this->assertResponseIsSuccessful(); From d4847dc9a02d1aa1e3dc0a68e2abfa3fc80afd9b Mon Sep 17 00:00:00 2001 From: soyuka Date: Thu, 18 Dec 2025 14:51:52 +0100 Subject: [PATCH 2/2] move argument to the end --- src/Symfony/Action/DocumentationAction.php | 2 +- src/Symfony/Bundle/Resources/config/symfony/controller.php | 2 +- src/Symfony/Bundle/Resources/config/symfony/events.php | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Symfony/Action/DocumentationAction.php b/src/Symfony/Action/DocumentationAction.php index 07c21353b4..891d5d83d4 100644 --- a/src/Symfony/Action/DocumentationAction.php +++ b/src/Symfony/Action/DocumentationAction.php @@ -50,8 +50,8 @@ public function __construct( ?Negotiator $negotiator = null, private readonly array $documentationFormats = [OpenApiNormalizer::JSON_FORMAT => ['application/vnd.openapi+json'], OpenApiNormalizer::FORMAT => ['application/json']], private readonly bool $swaggerUiEnabled = true, - private readonly bool $reDocEnabled = true, private readonly bool $docsEnabled = true, + private readonly bool $reDocEnabled = true, ) { $this->negotiator = $negotiator ?? new Negotiator(); } diff --git a/src/Symfony/Bundle/Resources/config/symfony/controller.php b/src/Symfony/Bundle/Resources/config/symfony/controller.php index d97adc6277..4fb1681354 100644 --- a/src/Symfony/Bundle/Resources/config/symfony/controller.php +++ b/src/Symfony/Bundle/Resources/config/symfony/controller.php @@ -48,7 +48,7 @@ service('api_platform.negotiator')->nullOnInvalid(), '%api_platform.docs_formats%', '%api_platform.enable_swagger_ui%', - '%api_platform.enable_re_doc%', '%api_platform.enable_docs%', + '%api_platform.enable_re_doc%', ]); }; diff --git a/src/Symfony/Bundle/Resources/config/symfony/events.php b/src/Symfony/Bundle/Resources/config/symfony/events.php index 9b07ff7dc5..0ec1a1daaf 100644 --- a/src/Symfony/Bundle/Resources/config/symfony/events.php +++ b/src/Symfony/Bundle/Resources/config/symfony/events.php @@ -180,8 +180,8 @@ service('api_platform.negotiator')->nullOnInvalid(), '%api_platform.docs_formats%', '%api_platform.enable_swagger_ui%', - '%api_platform.enable_re_doc%', '%api_platform.enable_docs%', + '%api_platform.enable_re_doc%', ]); $services->set('api_platform.action.placeholder', 'ApiPlatform\Symfony\Action\PlaceholderAction')