From add42cbe28ca63769449e80020f5ac83a3d0317a Mon Sep 17 00:00:00 2001 From: Jelle van Oosterbosch Date: Sun, 30 Aug 2026 17:20:33 +0200 Subject: [PATCH 1/3] Fail with an actionable error when NelmioApiDocBundle is unconfigured CollectNelmioApiDocRoutesPass returned early when the `nelmio_api_doc.areas` parameter was missing, so it never registered the `stixx_openapi_command.nelmio.routes_locator` service or the `stixx_openapi_command.nelmio.path_patterns` parameter. config/routing.php references both unconditionally, so the container failed to compile with an opaque "service does not exist" error during cache:clear. This is easy to hit when NelmioApiDocBundle's Flex recipe is skipped: Composer installs it, but it is never registered in config/bundles.php and never gets a package config. Throw a LogicException instead, distinguishing "not registered" from "registered but not configured" and including the bundles.php entry and a minimal nelmio_api_doc.yaml. Document the requirement in the README. --- README.md | 26 ++++++++++++++++ .../CollectNelmioApiDocRoutesPass.php | 30 ++++++++++++++++++- .../CollectNelmioApiDocRoutesPassTest.php | 24 +++++++++++++-- 3 files changed, 76 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 68f9cb8..a87ff59 100644 --- a/README.md +++ b/README.md @@ -37,6 +37,31 @@ return [ ]; ``` +### 3. Configure NelmioApiDocBundle + +This bundle reads its routes from NelmioApiDocBundle's areas, so Nelmio must be **registered and +configured** — installing it via Composer is not enough. Its Flex recipe normally does this, but if +the recipe was skipped you have to do it by hand. Add it to `config/bundles.php`: + +```php +return [ + // ... + Nelmio\ApiDocBundle\NelmioApiDocBundle::class => ['all' => true], +]; +``` + +And define at least one area in `config/packages/nelmio_api_doc.yaml`: + +```yaml +nelmio_api_doc: + areas: + default: + path_patterns: ['^/api'] +``` + +Without this, the container fails to compile and `cache:clear` reports that +`stixx_openapi_command.nelmio.routes_locator` does not exist. + ## Usage ### 1. Create a Command DTO @@ -161,6 +186,7 @@ See [Extension Points](docs/extension-points.md) for a worked example of each ex - PHP 8.4+ - Symfony 7.3+ or 8.0+ +- NelmioApiDocBundle 5.8+, registered and configured with at least one area ## License diff --git a/src/DependencyInjection/Compiler/CollectNelmioApiDocRoutesPass.php b/src/DependencyInjection/Compiler/CollectNelmioApiDocRoutesPass.php index a7fbf79..3154f0f 100644 --- a/src/DependencyInjection/Compiler/CollectNelmioApiDocRoutesPass.php +++ b/src/DependencyInjection/Compiler/CollectNelmioApiDocRoutesPass.php @@ -16,6 +16,7 @@ use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Definition; +use Symfony\Component\DependencyInjection\Exception\LogicException; use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\DependencyInjection\ServiceLocator; @@ -27,7 +28,7 @@ final class CollectNelmioApiDocRoutesPass implements CompilerPassInterface public function process(ContainerBuilder $container): void { if (!$container->hasParameter('nelmio_api_doc.areas')) { - return; + throw new LogicException($this->missingNelmioMessage($container)); } /** @var list $areas */ @@ -54,6 +55,33 @@ public function process(ContainerBuilder $container): void $container->setParameter('stixx_openapi_command.nelmio.path_patterns', $pathPatterns); } + /** + * Without `nelmio_api_doc.areas` we cannot register the routes locator, and the services in + * config/routing.php that depend on it fail with an unhelpful "service does not exist" error + * during cache:clear. Tell the user which half of the setup is missing instead. + * + * The typical cause is a skipped Flex recipe: NelmioApiDocBundle ends up installed by Composer + * but never registered in config/bundles.php, or registered without a package config. + */ + private function missingNelmioMessage(ContainerBuilder $container): string + { + $cause = $container->hasExtension('nelmio_api_doc') + ? 'NelmioApiDocBundle is registered but has no configuration, so it defined no areas.' + : 'NelmioApiDocBundle is not registered in config/bundles.php (its Flex recipe may have been skipped).'; + + return sprintf( + '%s %s requires it to be both registered and configured. Add ' + .'"Nelmio\ApiDocBundle\NelmioApiDocBundle::class => [\'all\' => true]" to config/bundles.php and create ' + ."config/packages/nelmio_api_doc.yaml with at least one area, for example:\n\n" + ."nelmio_api_doc:\n" + ." areas:\n" + ." default:\n" + ." path_patterns: ['^/api']\n", + $cause, + 'StixxOpenApiCommandBundle', + ); + } + /** * Reads `path_patterns` from the FilteredRouteCollectionBuilder factory definition that Nelmio * registers per area. When the area has no filter config, Nelmio uses the full router collection diff --git a/tests/Unit/DependencyInjection/Compiler/CollectNelmioApiDocRoutesPassTest.php b/tests/Unit/DependencyInjection/Compiler/CollectNelmioApiDocRoutesPassTest.php index f22ee22..e8e66b8 100644 --- a/tests/Unit/DependencyInjection/Compiler/CollectNelmioApiDocRoutesPassTest.php +++ b/tests/Unit/DependencyInjection/Compiler/CollectNelmioApiDocRoutesPassTest.php @@ -13,12 +13,14 @@ namespace Stixx\OpenApiCommandBundle\Tests\Unit\DependencyInjection\Compiler; +use Nelmio\ApiDocBundle\DependencyInjection\NelmioApiDocExtension; use Nelmio\ApiDocBundle\Routing\FilteredRouteCollectionBuilder; use PHPUnit\Framework\TestCase; use stdClass; use Stixx\OpenApiCommandBundle\DependencyInjection\Compiler\CollectNelmioApiDocRoutesPass; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Definition; +use Symfony\Component\DependencyInjection\Exception\LogicException; use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\DependencyInjection\ServiceLocator; use Symfony\Component\Routing\RouteCollection; @@ -60,18 +62,34 @@ public function testProcessWithAreas(): void ); } - public function testProcessWithoutParameter(): void + public function testProcessFailsWhenNelmioBundleIsNotRegistered(): void { // Arrange $container = new ContainerBuilder(); $pass = new CollectNelmioApiDocRoutesPass(); + // Assert + $this->expectException(LogicException::class); + $this->expectExceptionMessage('NelmioApiDocBundle is not registered in config/bundles.php'); + // Act $pass->process($container); + } + + public function testProcessFailsWhenNelmioBundleIsRegisteredButNotConfigured(): void + { + // Arrange — the extension exists (bundle in bundles.php) but no config was loaded, + // so Nelmio never set the `nelmio_api_doc.areas` parameter. + $container = new ContainerBuilder(); + $container->registerExtension(new NelmioApiDocExtension()); + $pass = new CollectNelmioApiDocRoutesPass(); // Assert - self::assertFalse($container->hasDefinition('stixx_openapi_command.nelmio.routes_locator')); - self::assertFalse($container->hasParameter('stixx_openapi_command.nelmio.path_patterns')); + $this->expectException(LogicException::class); + $this->expectExceptionMessage('NelmioApiDocBundle is registered but has no configuration'); + + // Act + $pass->process($container); } public function testExtractsPathPatternsFromFilteredRouteCollectionBuilderFactory(): void From b65c6aa638b59f41bc762507440257353953c2d4 Mon Sep 17 00:00:00 2001 From: Jelle van Oosterbosch Date: Sun, 30 Aug 2026 17:35:08 +0200 Subject: [PATCH 2/3] Address review feedback on the Nelmio setup error Make the remedy branch-specific: only the unregistered branch asks for a config/bundles.php entry, since the other branch has the bundle registered already. The README described the pre-fix missing-service error rather than the exception now raised. Collapse the two message tests into a data provider and trim comments. --- README.md | 5 +- .../CollectNelmioApiDocRoutesPass.php | 34 ++++----- .../CollectNelmioApiDocRoutesPassTest.php | 70 ++++++++++++++----- 3 files changed, 69 insertions(+), 40 deletions(-) diff --git a/README.md b/README.md index a87ff59..11d1c8b 100644 --- a/README.md +++ b/README.md @@ -59,8 +59,9 @@ nelmio_api_doc: path_patterns: ['^/api'] ``` -Without this, the container fails to compile and `cache:clear` reports that -`stixx_openapi_command.nelmio.routes_locator` does not exist. +Without this, the container fails to compile and `cache:clear` reports which part +is missing — the `config/bundles.php` entry or the package config — along with +the configuration to add. ## Usage diff --git a/src/DependencyInjection/Compiler/CollectNelmioApiDocRoutesPass.php b/src/DependencyInjection/Compiler/CollectNelmioApiDocRoutesPass.php index 3154f0f..963d1e9 100644 --- a/src/DependencyInjection/Compiler/CollectNelmioApiDocRoutesPass.php +++ b/src/DependencyInjection/Compiler/CollectNelmioApiDocRoutesPass.php @@ -56,30 +56,26 @@ public function process(ContainerBuilder $container): void } /** - * Without `nelmio_api_doc.areas` we cannot register the routes locator, and the services in - * config/routing.php that depend on it fail with an unhelpful "service does not exist" error - * during cache:clear. Tell the user which half of the setup is missing instead. - * - * The typical cause is a skipped Flex recipe: NelmioApiDocBundle ends up installed by Composer - * but never registered in config/bundles.php, or registered without a package config. + * Without the areas parameter the routes locator is never registered, and config/routing.php fails with + * an opaque "service does not exist" error. Name the missing half of the setup instead. */ private function missingNelmioMessage(ContainerBuilder $container): string { - $cause = $container->hasExtension('nelmio_api_doc') - ? 'NelmioApiDocBundle is registered but has no configuration, so it defined no areas.' - : 'NelmioApiDocBundle is not registered in config/bundles.php (its Flex recipe may have been skipped).'; - - return sprintf( - '%s %s requires it to be both registered and configured. Add ' - .'"Nelmio\ApiDocBundle\NelmioApiDocBundle::class => [\'all\' => true]" to config/bundles.php and create ' - ."config/packages/nelmio_api_doc.yaml with at least one area, for example:\n\n" - ."nelmio_api_doc:\n" + $example = "\n\nnelmio_api_doc:\n" ." areas:\n" ." default:\n" - ." path_patterns: ['^/api']\n", - $cause, - 'StixxOpenApiCommandBundle', - ); + ." path_patterns: ['^/api']\n"; + + if ($container->hasExtension('nelmio_api_doc')) { + return 'NelmioApiDocBundle is registered but has no configuration, so it defined no areas. ' + .'StixxOpenApiCommandBundle needs at least one area. Create config/packages/nelmio_api_doc.yaml ' + .'with, for example:'.$example; + } + + return 'NelmioApiDocBundle is not registered in config/bundles.php (its Flex recipe may have been ' + .'skipped). StixxOpenApiCommandBundle requires it to be both registered and configured. Add ' + .'"Nelmio\ApiDocBundle\NelmioApiDocBundle::class => [\'all\' => true]" to config/bundles.php, then ' + .'create config/packages/nelmio_api_doc.yaml with at least one area, for example:'.$example; } /** diff --git a/tests/Unit/DependencyInjection/Compiler/CollectNelmioApiDocRoutesPassTest.php b/tests/Unit/DependencyInjection/Compiler/CollectNelmioApiDocRoutesPassTest.php index e8e66b8..9582b8d 100644 --- a/tests/Unit/DependencyInjection/Compiler/CollectNelmioApiDocRoutesPassTest.php +++ b/tests/Unit/DependencyInjection/Compiler/CollectNelmioApiDocRoutesPassTest.php @@ -15,6 +15,7 @@ use Nelmio\ApiDocBundle\DependencyInjection\NelmioApiDocExtension; use Nelmio\ApiDocBundle\Routing\FilteredRouteCollectionBuilder; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; use stdClass; use Stixx\OpenApiCommandBundle\DependencyInjection\Compiler\CollectNelmioApiDocRoutesPass; @@ -62,34 +63,65 @@ public function testProcessWithAreas(): void ); } - public function testProcessFailsWhenNelmioBundleIsNotRegistered(): void + /** + * @param list $expected + * @param list $notExpected + */ + #[DataProvider('missingSetupProvider')] + public function testProcessFailsWhenNelmioIsNotConfigured(bool $registered, array $expected, array $notExpected): void { - // Arrange + // Arrange — a registered bundle has an extension; without config it still sets no areas parameter. $container = new ContainerBuilder(); - $pass = new CollectNelmioApiDocRoutesPass(); + if ($registered) { + $container->registerExtension(new NelmioApiDocExtension()); + } + + // Act + $message = null; + + try { + (new CollectNelmioApiDocRoutesPass())->process($container); + } catch (LogicException $exception) { + $message = $exception->getMessage(); + } // Assert - $this->expectException(LogicException::class); - $this->expectExceptionMessage('NelmioApiDocBundle is not registered in config/bundles.php'); + self::assertNotNull($message, 'Expected a LogicException when nelmio_api_doc.areas is missing.'); - // Act - $pass->process($container); + foreach ($expected as $needle) { + self::assertStringContainsString($needle, $message); + } + + foreach ($notExpected as $needle) { + self::assertStringNotContainsString($needle, $message); + } } - public function testProcessFailsWhenNelmioBundleIsRegisteredButNotConfigured(): void + /** + * @return iterable, list}> + */ + public static function missingSetupProvider(): iterable { - // Arrange — the extension exists (bundle in bundles.php) but no config was loaded, - // so Nelmio never set the `nelmio_api_doc.areas` parameter. - $container = new ContainerBuilder(); - $container->registerExtension(new NelmioApiDocExtension()); - $pass = new CollectNelmioApiDocRoutesPass(); - - // Assert - $this->expectException(LogicException::class); - $this->expectExceptionMessage('NelmioApiDocBundle is registered but has no configuration'); + yield 'bundle not registered' => [ + false, + [ + 'NelmioApiDocBundle is not registered in config/bundles.php', + 'NelmioApiDocBundle::class', + 'config/packages/nelmio_api_doc.yaml', + "path_patterns: ['^/api']", + ], + [], + ]; - // Act - $pass->process($container); + // Already registered, so the message must not ask for a config/bundles.php entry. + yield 'bundle registered without config' => [ + true, + [ + 'NelmioApiDocBundle is registered but has no configuration', + 'config/packages/nelmio_api_doc.yaml', + ], + ['config/bundles.php'], + ]; } public function testExtractsPathPatternsFromFilteredRouteCollectionBuilderFactory(): void From c8191f0d743501d63bb84c6d7991c982631f51f9 Mon Sep 17 00:00:00 2001 From: Jelle van Oosterbosch Date: Sun, 30 Aug 2026 17:56:45 +0200 Subject: [PATCH 3/3] Cover the missing-Nelmio failure with a functional test The unit tests call the compiler pass directly, so they cannot show that the exception survives a real container build. The bundle's own prepend() writes nelmio_api_doc config too, so something upstream could fail first and the pass would never run. Booting a kernel without NelmioApiDocBundle now asserts the actionable message. With the throw reverted, this test fails with the original symptom: a missing stixx_openapi_command.nelmio.routes_locator service. --- tests/Functional/App/MissingNelmioKernel.php | 39 ++++++++++++++++++ .../MissingNelmioConfigurationTest.php | 40 +++++++++++++++++++ .../Resources/config/without_nelmio.php | 33 +++++++++++++++ 3 files changed, 112 insertions(+) create mode 100644 tests/Functional/App/MissingNelmioKernel.php create mode 100644 tests/Functional/MissingNelmioConfigurationTest.php create mode 100644 tests/Functional/Resources/config/without_nelmio.php diff --git a/tests/Functional/App/MissingNelmioKernel.php b/tests/Functional/App/MissingNelmioKernel.php new file mode 100644 index 0000000..a99b60e --- /dev/null +++ b/tests/Functional/App/MissingNelmioKernel.php @@ -0,0 +1,39 @@ +addTestBundle(FrameworkBundle::class); + // NelmioApiDocBundle is deliberately not registered. + $this->addTestBundle(StixxOpenApiCommandBundle::class); + $this->addTestConfig(__DIR__.'/../Resources/config/without_nelmio.php'); + } + + public function getProjectDir(): string + { + return __DIR__.'/../'; + } +} diff --git a/tests/Functional/MissingNelmioConfigurationTest.php b/tests/Functional/MissingNelmioConfigurationTest.php new file mode 100644 index 0000000..d5b9ff3 --- /dev/null +++ b/tests/Functional/MissingNelmioConfigurationTest.php @@ -0,0 +1,40 @@ +expectException(LogicException::class); + $this->expectExceptionMessage('NelmioApiDocBundle is not registered in config/bundles.php'); + + // Act + $kernel->boot(); + } +} diff --git a/tests/Functional/Resources/config/without_nelmio.php b/tests/Functional/Resources/config/without_nelmio.php new file mode 100644 index 0000000..673d56b --- /dev/null +++ b/tests/Functional/Resources/config/without_nelmio.php @@ -0,0 +1,33 @@ +extension('framework', [ + 'test' => true, + 'messenger' => [ + 'enabled' => true, + ], + 'serializer' => [ + 'enabled' => true, + ], + 'validation' => [ + 'enabled' => true, + ], + 'http_method_override' => false, + 'php_errors' => [ + 'log' => false, + ], + ]); +};