diff --git a/README.md b/README.md index 68f9cb8..11d1c8b 100644 --- a/README.md +++ b/README.md @@ -37,6 +37,32 @@ 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 which part +is missing — the `config/bundles.php` entry or the package config — along with +the configuration to add. + ## Usage ### 1. Create a Command DTO @@ -161,6 +187,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..963d1e9 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,29 @@ public function process(ContainerBuilder $container): void $container->setParameter('stixx_openapi_command.nelmio.path_patterns', $pathPatterns); } + /** + * 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 + { + $example = "\n\nnelmio_api_doc:\n" + ." areas:\n" + ." default:\n" + ." 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; + } + /** * 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/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, + ], + ]); +}; diff --git a/tests/Unit/DependencyInjection/Compiler/CollectNelmioApiDocRoutesPassTest.php b/tests/Unit/DependencyInjection/Compiler/CollectNelmioApiDocRoutesPassTest.php index f22ee22..9582b8d 100644 --- a/tests/Unit/DependencyInjection/Compiler/CollectNelmioApiDocRoutesPassTest.php +++ b/tests/Unit/DependencyInjection/Compiler/CollectNelmioApiDocRoutesPassTest.php @@ -13,12 +13,15 @@ namespace Stixx\OpenApiCommandBundle\Tests\Unit\DependencyInjection\Compiler; +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; 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 +63,65 @@ public function testProcessWithAreas(): void ); } - public function testProcessWithoutParameter(): 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 - $pass->process($container); + $message = null; + + try { + (new CollectNelmioApiDocRoutesPass())->process($container); + } catch (LogicException $exception) { + $message = $exception->getMessage(); + } // Assert - self::assertFalse($container->hasDefinition('stixx_openapi_command.nelmio.routes_locator')); - self::assertFalse($container->hasParameter('stixx_openapi_command.nelmio.path_patterns')); + self::assertNotNull($message, 'Expected a LogicException when nelmio_api_doc.areas is missing.'); + + foreach ($expected as $needle) { + self::assertStringContainsString($needle, $message); + } + + foreach ($notExpected as $needle) { + self::assertStringNotContainsString($needle, $message); + } + } + + /** + * @return iterable, list}> + */ + public static function missingSetupProvider(): iterable + { + yield 'bundle not registered' => [ + false, + [ + 'NelmioApiDocBundle is not registered in config/bundles.php', + 'NelmioApiDocBundle::class', + 'config/packages/nelmio_api_doc.yaml', + "path_patterns: ['^/api']", + ], + [], + ]; + + // 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