Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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<string> $areas */
Expand All @@ -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
Expand Down
39 changes: 39 additions & 0 deletions tests/Functional/App/MissingNelmioKernel.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<?php

declare(strict_types=1);

/*
* This file is part of the StixxOpenApiCommandBundle package.
*
* (c) Stixx
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Stixx\OpenApiCommandBundle\Tests\Functional\App;

use Nyholm\BundleTest\TestKernel;
use Stixx\OpenApiCommandBundle\StixxOpenApiCommandBundle;
use Symfony\Bundle\FrameworkBundle\FrameworkBundle;

/**
* A kernel without NelmioApiDocBundle, reproducing a skipped Flex recipe.
*/
class MissingNelmioKernel extends TestKernel
{
public function __construct(string $environment, bool $debug)
{
parent::__construct($environment, $debug);

$this->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__.'/../';
}
}
40 changes: 40 additions & 0 deletions tests/Functional/MissingNelmioConfigurationTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<?php

declare(strict_types=1);

/*
* This file is part of the StixxOpenApiCommandBundle package.
*
* (c) Stixx
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Stixx\OpenApiCommandBundle\Tests\Functional;

use PHPUnit\Framework\Attributes\WithoutErrorHandler;
use Stixx\OpenApiCommandBundle\Tests\Functional\App\MissingNelmioKernel;
use Symfony\Component\DependencyInjection\Exception\LogicException;

/**
* The unit tests call the compiler pass directly, which cannot show whether the exception survives a real
* container build — the bundle's own prepend() also writes nelmio_api_doc config, so something upstream
* could fail first with a different error and the pass would never run.
*/
final class MissingNelmioConfigurationTest extends AbstractKernelTestCase
{
#[WithoutErrorHandler]
public function testBootingWithoutNelmioApiDocBundleExplainsHowToFixIt(): void
{
// Arrange
$kernel = new MissingNelmioKernel('test', true);

// Assert
$this->expectException(LogicException::class);
$this->expectExceptionMessage('NelmioApiDocBundle is not registered in config/bundles.php');

// Act
$kernel->boot();
}
}
33 changes: 33 additions & 0 deletions tests/Functional/Resources/config/without_nelmio.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php

declare(strict_types=1);

/*
* This file is part of the StixxOpenApiCommandBundle package.
*
* (c) Stixx
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;

return static function (ContainerConfigurator $container): void {
$container->extension('framework', [
'test' => true,
'messenger' => [
'enabled' => true,
],
'serializer' => [
'enabled' => true,
],
'validation' => [
'enabled' => true,
],
'http_method_override' => false,
'php_errors' => [
'log' => false,
],
]);
};
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -60,18 +63,65 @@ public function testProcessWithAreas(): void
);
}

public function testProcessWithoutParameter(): void
/**
* @param list<string> $expected
* @param list<string> $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<string, array{bool, list<string>, list<string>}>
*/
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
Expand Down
Loading