From 419e0216f0b563a9286edbbbe653ad1701c356e0 Mon Sep 17 00:00:00 2001 From: Jan Esser Date: Fri, 31 Oct 2025 08:36:17 +0100 Subject: [PATCH] feat(symfony): allow symfony makers namespace configuration --- .../ApiPlatformExtension.php | 6 ++++ .../DependencyInjection/Configuration.php | 3 ++ src/Symfony/Bundle/Resources/config/maker.php | 6 ++-- src/Symfony/Maker/MakeFilter.php | 8 ++++- src/Symfony/Maker/MakeStateProcessor.php | 8 ++++- src/Symfony/Maker/MakeStateProvider.php | 8 ++++- .../Maker/NamespacedCustomOrmFilter.fixture | 29 +++++++++++++++++++ .../NamespacedCustomStateProcessor.fixture | 12 ++++++++ .../NamespacedCustomStateProvider.fixture | 12 ++++++++ .../DependencyInjection/ConfigurationTest.php | 1 + tests/Symfony/Maker/MakeFilterTest.php | 19 ++++++++++++ .../Symfony/Maker/MakeStateProcessorTest.php | 19 ++++++++++++ tests/Symfony/Maker/MakeStateProviderTest.php | 19 ++++++++++++ 13 files changed, 144 insertions(+), 6 deletions(-) create mode 100644 tests/Fixtures/Symfony/Maker/NamespacedCustomOrmFilter.fixture create mode 100644 tests/Fixtures/Symfony/Maker/NamespacedCustomStateProcessor.fixture create mode 100644 tests/Fixtures/Symfony/Maker/NamespacedCustomStateProvider.fixture diff --git a/src/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php b/src/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php index f2b9da89b29..b111384e1a2 100644 --- a/src/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php +++ b/src/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php @@ -964,6 +964,12 @@ private function registerMakerConfiguration(ContainerBuilder $container, array $ return; } + $namespaceprefix = $config['maker']['namespace_prefix'] ?? ''; + if ('' !== $namespaceprefix) { + $namespaceprefix = trim($namespaceprefix, '\\').'\\'; + } + $container->setParameter('api_platform.maker.namespace_prefix', $namespaceprefix); + $loader->load('maker.php'); } diff --git a/src/Symfony/Bundle/DependencyInjection/Configuration.php b/src/Symfony/Bundle/DependencyInjection/Configuration.php index 83cf3330c0f..33ccd4ba26c 100644 --- a/src/Symfony/Bundle/DependencyInjection/Configuration.php +++ b/src/Symfony/Bundle/DependencyInjection/Configuration.php @@ -657,6 +657,9 @@ private function addMakerSection(ArrayNodeDefinition $rootNode): void ->children() ->arrayNode('maker') ->{class_exists(MakerBundle::class) ? 'canBeDisabled' : 'canBeEnabled'}() + ->children() + ->scalarNode('namespace_prefix')->defaultValue('')->info('Add a prefix to all maker generated classes. e.g set it to "Api" to set the maker namespace to "App\\Api\\" (if the maker.root_namespace config is App). e.g. App\\Api\\State\\MyStateProcessor')->end() + ->end() ->end() ->end(); } diff --git a/src/Symfony/Bundle/Resources/config/maker.php b/src/Symfony/Bundle/Resources/config/maker.php index fad89af1b9b..e05ec7520df 100644 --- a/src/Symfony/Bundle/Resources/config/maker.php +++ b/src/Symfony/Bundle/Resources/config/maker.php @@ -17,14 +17,14 @@ $services = $container->services(); $services->set('api_platform.maker.command.state_processor', 'ApiPlatform\Symfony\Maker\MakeStateProcessor') - ->args([service('api_platform.metadata.resource.name_collection_factory')]) + ->args([param('api_platform.maker.namespace_prefix')]) ->tag('maker.command'); $services->set('api_platform.maker.command.state_provider', 'ApiPlatform\Symfony\Maker\MakeStateProvider') - ->args([service('api_platform.metadata.resource.name_collection_factory')]) + ->args([param('api_platform.maker.namespace_prefix')]) ->tag('maker.command'); $services->set('api_platform.maker.command.filter', 'ApiPlatform\Symfony\Maker\MakeFilter') - ->args([service('api_platform.metadata.resource.name_collection_factory')]) + ->args([param('api_platform.maker.namespace_prefix')]) ->tag('maker.command'); }; diff --git a/src/Symfony/Maker/MakeFilter.php b/src/Symfony/Maker/MakeFilter.php index e2796c7f5e2..30397289429 100644 --- a/src/Symfony/Maker/MakeFilter.php +++ b/src/Symfony/Maker/MakeFilter.php @@ -23,9 +23,14 @@ use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; final class MakeFilter extends AbstractMaker { + public function __construct(private readonly string $namespacePrefix = '') + { + } + /** * {@inheritdoc} */ @@ -50,6 +55,7 @@ public function configureCommand(Command $command, InputConfiguration $inputConf $command ->addArgument('type', InputArgument::REQUIRED, \sprintf('Choose a type for your filter (%s)', self::getFilterTypesAsString())) ->addArgument('name', InputArgument::REQUIRED, 'Choose a class name for your filter (e.g. AwesomeFilter)') + ->addOption('namespace-prefix', 'p', InputOption::VALUE_REQUIRED, 'Specify the namespace prefix to use for the filter class', $this->namespacePrefix.'Filter') ->setHelp(file_get_contents(__DIR__.'/Resources/help/MakeFilter.txt')); } @@ -75,7 +81,7 @@ public function generate(InputInterface $input, ConsoleStyle $io, Generator $gen $filterNameDetails = $generator->createClassNameDetails( name: $input->getArgument('name'), - namespacePrefix: 'Filter\\' + namespacePrefix: trim($input->getOption('namespace-prefix'), '\\').'\\' ); $filterName = \sprintf('%sFilter', ucfirst($type->value)); diff --git a/src/Symfony/Maker/MakeStateProcessor.php b/src/Symfony/Maker/MakeStateProcessor.php index 82c8596a952..9cad24cc56a 100644 --- a/src/Symfony/Maker/MakeStateProcessor.php +++ b/src/Symfony/Maker/MakeStateProcessor.php @@ -21,9 +21,14 @@ use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; final class MakeStateProcessor extends AbstractMaker { + public function __construct(private readonly string $namespacePrefix = '') + { + } + /** * {@inheritdoc} */ @@ -47,6 +52,7 @@ public function configureCommand(Command $command, InputConfiguration $inputConf { $command ->addArgument('name', InputArgument::REQUIRED, 'Choose a class name for your state processor (e.g. AwesomeStateProcessor)') + ->addOption('namespace-prefix', 'p', InputOption::VALUE_REQUIRED, 'Specify the namespace prefix to use for the state processor class', $this->namespacePrefix.'State') ->setHelp(file_get_contents(__DIR__.'/Resources/help/MakeStateProcessor.txt')); } @@ -64,7 +70,7 @@ public function generate(InputInterface $input, ConsoleStyle $io, Generator $gen { $stateProcessorClassNameDetails = $generator->createClassNameDetails( $input->getArgument('name'), - 'State\\' + trim($input->getOption('namespace-prefix'), '\\').'\\' ); $generator->generateClass( diff --git a/src/Symfony/Maker/MakeStateProvider.php b/src/Symfony/Maker/MakeStateProvider.php index 7fc69cd938a..c5297e9547c 100644 --- a/src/Symfony/Maker/MakeStateProvider.php +++ b/src/Symfony/Maker/MakeStateProvider.php @@ -21,9 +21,14 @@ use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; final class MakeStateProvider extends AbstractMaker { + public function __construct(private readonly string $namespacePrefix = '') + { + } + /** * {@inheritdoc} */ @@ -47,6 +52,7 @@ public function configureCommand(Command $command, InputConfiguration $inputConf { $command ->addArgument('name', InputArgument::REQUIRED, 'Choose a class name for your state provider (e.g. AwesomeStateProvider)') + ->addOption('namespace-prefix', 'p', InputOption::VALUE_REQUIRED, 'Specify the namespace prefix to use for the state provider class', $this->namespacePrefix.'State') ->setHelp(file_get_contents(__DIR__.'/Resources/help/MakeStateProvider.txt')); } @@ -64,7 +70,7 @@ public function generate(InputInterface $input, ConsoleStyle $io, Generator $gen { $stateProviderClassNameDetails = $generator->createClassNameDetails( $input->getArgument('name'), - 'State\\' + trim($input->getOption('namespace-prefix'), '\\').'\\' ); $generator->generateClass( diff --git a/tests/Fixtures/Symfony/Maker/NamespacedCustomOrmFilter.fixture b/tests/Fixtures/Symfony/Maker/NamespacedCustomOrmFilter.fixture new file mode 100644 index 00000000000..15b5ae45cef --- /dev/null +++ b/tests/Fixtures/Symfony/Maker/NamespacedCustomOrmFilter.fixture @@ -0,0 +1,29 @@ +namespace App\Api\Filter; + +use ApiPlatform\Doctrine\Orm\Filter\FilterInterface; +use ApiPlatform\Doctrine\Orm\Util\QueryNameGeneratorInterface; +use ApiPlatform\Metadata\BackwardCompatibleFilterDescriptionTrait; +use ApiPlatform\Metadata\Operation; +use Doctrine\ORM\QueryBuilder; + +class CustomOrmFilter implements FilterInterface +{ + use BackwardCompatibleFilterDescriptionTrait; // Here for backward compatibility, keep it until 5.0. + + public function apply(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, ?Operation $operation = null, array $context = []): void + { + // Retrieve the parameter and it's value + // $parameter = $context['parameter']; + // $value = $parameter->getValue(); + + // Retrieve the property + // $property = $parameter->getProperty(); + + // Retrieve alias and parameter name + // $alias = $queryBuilder->getRootAliases()[0]; + // $parameterName = $queryNameGenerator->generateParameterName($property); + + // TODO: make your awesome query using the $queryBuilder + // $queryBuilder-> + } +} diff --git a/tests/Fixtures/Symfony/Maker/NamespacedCustomStateProcessor.fixture b/tests/Fixtures/Symfony/Maker/NamespacedCustomStateProcessor.fixture new file mode 100644 index 00000000000..0885a8f6f8a --- /dev/null +++ b/tests/Fixtures/Symfony/Maker/NamespacedCustomStateProcessor.fixture @@ -0,0 +1,12 @@ +namespace App\Api\State; + +use ApiPlatform\Metadata\Operation; +use ApiPlatform\State\ProcessorInterface; + +class CustomStateProcessor implements ProcessorInterface +{ + public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): void + { + // Handle the state + } +} diff --git a/tests/Fixtures/Symfony/Maker/NamespacedCustomStateProvider.fixture b/tests/Fixtures/Symfony/Maker/NamespacedCustomStateProvider.fixture new file mode 100644 index 00000000000..e4606243152 --- /dev/null +++ b/tests/Fixtures/Symfony/Maker/NamespacedCustomStateProvider.fixture @@ -0,0 +1,12 @@ +namespace App\Api\State; + +use ApiPlatform\Metadata\Operation; +use ApiPlatform\State\ProviderInterface; + +class CustomStateProvider implements ProviderInterface +{ + public function provide(Operation $operation, array $uriVariables = [], array $context = []): object|array|null + { + // Retrieve the state from somewhere + } +} diff --git a/tests/Symfony/Bundle/DependencyInjection/ConfigurationTest.php b/tests/Symfony/Bundle/DependencyInjection/ConfigurationTest.php index e3c4e91add8..7a8a7b54cd4 100644 --- a/tests/Symfony/Bundle/DependencyInjection/ConfigurationTest.php +++ b/tests/Symfony/Bundle/DependencyInjection/ConfigurationTest.php @@ -229,6 +229,7 @@ private function runDefaultConfigTests(array $doctrineIntegrationsToLoad = ['orm ], 'maker' => [ 'enabled' => true, + 'namespace_prefix' => '', ], 'use_symfony_listeners' => false, 'handle_symfony_errors' => false, diff --git a/tests/Symfony/Maker/MakeFilterTest.php b/tests/Symfony/Maker/MakeFilterTest.php index bf6bd01ea5b..1755ffab2ab 100644 --- a/tests/Symfony/Maker/MakeFilterTest.php +++ b/tests/Symfony/Maker/MakeFilterTest.php @@ -61,6 +61,25 @@ public function testMakeFilter(string $type, string $name, bool $isInteractive): $this->assertStringContainsString(' Next: Open your filter class and start customizing it.', $display); } + public function testMakeFilterWithCustomNamespace(): void + { + $inputs = ['name' => 'CustomOrmFilter', 'type' => 'orm', '--namespace-prefix' => 'Api\\Filter\\']; + $newFilterFile = self::tempFile('src/Api/Filter/CustomOrmFilter.php'); + + $tester = new CommandTester((new Application(self::bootKernel()))->find('make:filter')); + $tester->execute($inputs); + + $this->assertFileExists($newFilterFile); + + // Unify line endings + $expected = preg_replace('~\R~u', "\r\n", file_get_contents(__DIR__.'/../../Fixtures/Symfony/Maker/NamespacedCustomOrmFilter.fixture')); + $result = preg_replace('~\R~u', "\r\n", file_get_contents($newFilterFile)); + $this->assertStringContainsString($expected, $result); + + $display = $tester->getDisplay(); + $this->assertStringContainsString('Success!', $display); + } + public static function filterProvider(): \Generator { yield 'Generate ORM filter' => ['orm', 'CustomOrmFilter', true]; diff --git a/tests/Symfony/Maker/MakeStateProcessorTest.php b/tests/Symfony/Maker/MakeStateProcessorTest.php index b88cbbce43c..18a170c031d 100644 --- a/tests/Symfony/Maker/MakeStateProcessorTest.php +++ b/tests/Symfony/Maker/MakeStateProcessorTest.php @@ -56,6 +56,25 @@ public function testMakeStateProcessor(bool $isInteractive): void $this->assertStringContainsString('Next: Open your new state processor class and start customizing it.', $display); } + public function testMakeStateProcessorWithCustomNamespace(): void + { + $inputs = ['name' => 'CustomStateProcessor', '--namespace-prefix' => 'Api\\State\\']; + $newProviderFile = self::tempFile('src/Api/State/CustomStateProcessor.php'); + + $tester = new CommandTester((new Application(self::bootKernel()))->find('make:state-processor')); + $tester->execute($inputs); + + $this->assertFileExists($newProviderFile); + + // Unify line endings + $expected = preg_replace('~\R~u', "\r\n", file_get_contents(__DIR__.'/../../Fixtures/Symfony/Maker/NamespacedCustomStateProcessor.fixture')); + $result = preg_replace('~\R~u', "\r\n", file_get_contents($newProviderFile)); + $this->assertStringContainsString($expected, $result); + + $display = $tester->getDisplay(); + $this->assertStringContainsString('Success!', $display); + } + public static function stateProcessorProvider(): \Generator { yield 'Generate state processor' => [ diff --git a/tests/Symfony/Maker/MakeStateProviderTest.php b/tests/Symfony/Maker/MakeStateProviderTest.php index 101dc080082..9665388e343 100644 --- a/tests/Symfony/Maker/MakeStateProviderTest.php +++ b/tests/Symfony/Maker/MakeStateProviderTest.php @@ -56,6 +56,25 @@ public function testMakeStateProvider(bool $isInteractive): void $this->assertStringContainsString('Next: Open your new state provider class and start customizing it.', $display); } + public function testMakeStateProviderWithCustomNamespace(): void + { + $inputs = ['name' => 'CustomStateProvider', '--namespace-prefix' => 'Api\\State\\']; + $newProviderFile = self::tempFile('src/Api/State/CustomStateProvider.php'); + + $tester = new CommandTester((new Application(self::bootKernel()))->find('make:state-provider')); + $tester->execute($inputs); + + $this->assertFileExists($newProviderFile); + + // Unify line endings + $expected = preg_replace('~\R~u', "\r\n", file_get_contents(__DIR__.'/../../Fixtures/Symfony/Maker/NamespacedCustomStateProvider.fixture')); + $result = preg_replace('~\R~u', "\r\n", file_get_contents($newProviderFile)); + $this->assertStringContainsString($expected, $result); + + $display = $tester->getDisplay(); + $this->assertStringContainsString('Success!', $display); + } + public static function stateProviderDataProvider(): \Generator { yield 'Generate state provider' => [