diff --git a/src/Symfony/Bundle/Resources/config/maker.xml b/src/Symfony/Bundle/Resources/config/maker.xml index 4b089817f9e..ce87f360c4c 100644 --- a/src/Symfony/Bundle/Resources/config/maker.xml +++ b/src/Symfony/Bundle/Resources/config/maker.xml @@ -14,6 +14,11 @@ + + + + + diff --git a/src/Symfony/Maker/Enum/SupportedFilterTypes.php b/src/Symfony/Maker/Enum/SupportedFilterTypes.php new file mode 100644 index 00000000000..94721ddb871 --- /dev/null +++ b/src/Symfony/Maker/Enum/SupportedFilterTypes.php @@ -0,0 +1,20 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Symfony\Maker\Enum; + +enum SupportedFilterTypes: string +{ + case ORM = 'orm'; + case ODM = 'odm'; +} diff --git a/src/Symfony/Maker/MakeFilter.php b/src/Symfony/Maker/MakeFilter.php new file mode 100644 index 00000000000..e2796c7f5e2 --- /dev/null +++ b/src/Symfony/Maker/MakeFilter.php @@ -0,0 +1,102 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Symfony\Maker; + +use ApiPlatform\Metadata\Exception\InvalidArgumentException; +use ApiPlatform\Symfony\Maker\Enum\SupportedFilterTypes; +use Symfony\Bundle\MakerBundle\ConsoleStyle; +use Symfony\Bundle\MakerBundle\DependencyBuilder; +use Symfony\Bundle\MakerBundle\Generator; +use Symfony\Bundle\MakerBundle\InputConfiguration; +use Symfony\Bundle\MakerBundle\Maker\AbstractMaker; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputInterface; + +final class MakeFilter extends AbstractMaker +{ + /** + * {@inheritdoc} + */ + public static function getCommandName(): string + { + return 'make:filter'; + } + + /** + * {@inheritdoc} + */ + public static function getCommandDescription(): string + { + return 'Creates an API Platform filter'; + } + + /** + * {@inheritdoc} + */ + public function configureCommand(Command $command, InputConfiguration $inputConfig): void + { + $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)') + ->setHelp(file_get_contents(__DIR__.'/Resources/help/MakeFilter.txt')); + } + + /** + * {@inheritdoc} + */ + public function configureDependencies(DependencyBuilder $dependencies): void + { + } + + /** + * {@inheritdoc} + * + * @throws \Exception + */ + public function generate(InputInterface $input, ConsoleStyle $io, Generator $generator): void + { + $typeArgument = strtolower((string) $input->getArgument('type')); + $type = SupportedFilterTypes::tryFrom($typeArgument); + if (null === $type) { + throw new InvalidArgumentException(\sprintf('The type "%s" is not a valid filter type, valid options are: %s.', $typeArgument, self::getFilterTypesAsString())); + } + + $filterNameDetails = $generator->createClassNameDetails( + name: $input->getArgument('name'), + namespacePrefix: 'Filter\\' + ); + $filterName = \sprintf('%sFilter', ucfirst($type->value)); + + $generator->generateClass(className: $filterNameDetails->getFullName(), templateName: \sprintf( + '%s/Resources/skeleton/%s.php.tpl', + __DIR__, + $filterName + )); + + $generator->writeChanges(); + + $this->writeSuccessMessage($io); + $io->text([ + 'Next: Open your filter class and start customizing it.', + ]); + } + + private static function getFilterTypesAsString(): string + { + $validOptions = array_column(SupportedFilterTypes::cases(), 'value'); + + return implode(' or ', array_map('strtoupper', $validOptions)); + } +} diff --git a/src/Symfony/Maker/Resources/help/MakeFilter.txt b/src/Symfony/Maker/Resources/help/MakeFilter.txt new file mode 100644 index 00000000000..d5b4e504e38 --- /dev/null +++ b/src/Symfony/Maker/Resources/help/MakeFilter.txt @@ -0,0 +1,9 @@ +The %command.name% command generates a new API Platform filter class for Doctrine ORM or ODM (MongoDB). + +php %command.full_name% type name + +Important: +- If you omit the argument, the command will prompt you to choose the filter type interactively. +- If you omit the argument, the command will ask you to enter the class name interactively. + +Elasticsearch isn't supported yet. diff --git a/src/Symfony/Maker/Resources/skeleton/OdmFilter.php.tpl b/src/Symfony/Maker/Resources/skeleton/OdmFilter.php.tpl new file mode 100644 index 00000000000..5cc29f54ef1 --- /dev/null +++ b/src/Symfony/Maker/Resources/skeleton/OdmFilter.php.tpl @@ -0,0 +1,27 @@ + + +namespace ; + +use ApiPlatform\Doctrine\Odm\Filter\FilterInterface; +use ApiPlatform\Metadata\BackwardCompatibleFilterDescriptionTrait; +use ApiPlatform\Metadata\Operation; +use Doctrine\ODM\MongoDB\Aggregation\Builder; + +class implements FilterInterface +{ + use BackwardCompatibleFilterDescriptionTrait; // Here for backward compatibility, keep it until 5.0. + + public function apply(Builder $aggregationBuilder, 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(); + + // TODO: make your awesome query using the $aggregationBuilder + // $aggregationBuilder-> + } +} diff --git a/src/Symfony/Maker/Resources/skeleton/OrmFilter.php.tpl b/src/Symfony/Maker/Resources/skeleton/OrmFilter.php.tpl new file mode 100644 index 00000000000..b4d8a82318a --- /dev/null +++ b/src/Symfony/Maker/Resources/skeleton/OrmFilter.php.tpl @@ -0,0 +1,32 @@ + + +namespace ; + +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 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/CustomOdmFilter.fixture b/tests/Fixtures/Symfony/Maker/CustomOdmFilter.fixture new file mode 100644 index 00000000000..502251bedac --- /dev/null +++ b/tests/Fixtures/Symfony/Maker/CustomOdmFilter.fixture @@ -0,0 +1,24 @@ +namespace App\Filter; + +use ApiPlatform\Doctrine\Odm\Filter\FilterInterface; +use ApiPlatform\Metadata\BackwardCompatibleFilterDescriptionTrait; +use ApiPlatform\Metadata\Operation; +use Doctrine\ODM\MongoDB\Aggregation\Builder; + +class CustomOdmFilter implements FilterInterface +{ + use BackwardCompatibleFilterDescriptionTrait; // Here for backward compatibility, keep it until 5.0. + + public function apply(Builder $aggregationBuilder, 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(); + + // TODO: make your awesome query using the $aggregationBuilder + // $aggregationBuilder-> + } +} diff --git a/tests/Fixtures/Symfony/Maker/CustomOrmFilter.fixture b/tests/Fixtures/Symfony/Maker/CustomOrmFilter.fixture new file mode 100644 index 00000000000..3750e1830b0 --- /dev/null +++ b/tests/Fixtures/Symfony/Maker/CustomOrmFilter.fixture @@ -0,0 +1,29 @@ +namespace App\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/Symfony/Maker/MakeFilterTest.php b/tests/Symfony/Maker/MakeFilterTest.php new file mode 100644 index 00000000000..bf6bd01ea5b --- /dev/null +++ b/tests/Symfony/Maker/MakeFilterTest.php @@ -0,0 +1,110 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Tests\Symfony\Maker; + +use ApiPlatform\Metadata\Exception\InvalidArgumentException; +use PHPUnit\Framework\Attributes\DataProvider; +use Symfony\Bundle\FrameworkBundle\Console\Application; +use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; +use Symfony\Component\Console\Exception\MissingInputException; +use Symfony\Component\Console\Tester\CommandTester; +use Symfony\Component\Filesystem\Filesystem; + +class MakeFilterTest extends KernelTestCase +{ + protected function setup(): void + { + (new Filesystem())->remove(self::tempDir()); + } + + #[DataProvider('filterProvider')] + public function testMakeFilter(string $type, string $name, bool $isInteractive): void + { + $inputs = ['type' => $type, 'name' => $name]; + $newFilterFile = self::tempFile("src/Filter/{$name}.php"); + + $command = (new Application(self::bootKernel()))->find('make:filter'); + $commandTester = new CommandTester($command); + $commandTester->setInputs($isInteractive ? $inputs : []); + $commandTester->execute($isInteractive ? [] : $inputs); + + $this->assertFileExists($newFilterFile); + + $expected = preg_replace('~\R~u', "\r\n", file_get_contents(__DIR__."/../../Fixtures/Symfony/Maker/{$name}.fixture")); + $result = preg_replace('~\R~u', "\r\n", file_get_contents($newFilterFile)); + $this->assertStringContainsString($expected, $result); + + $display = $commandTester->getDisplay(); + $commandTester->assertCommandIsSuccessful(); + $interactiveOutputType = 'Choose a type for your filter'; + $interactiveOutputName = 'Choose a class name for your filter'; + + if ($isInteractive) { + $this->assertStringContainsString($interactiveOutputType, $display); + $this->assertStringContainsString($interactiveOutputName, $display); + } else { + $this->assertStringNotContainsString($interactiveOutputType, $display); + $this->assertStringNotContainsString($interactiveOutputName, $display); + } + + $this->assertStringContainsString(' Next: Open your filter class and start customizing it.', $display); + } + + public static function filterProvider(): \Generator + { + yield 'Generate ORM filter' => ['orm', 'CustomOrmFilter', true]; + yield 'Generate ORM filter not interactively' => ['orm', 'CustomOrmFilter', false]; + yield 'Generate ODM filter' => ['odm', 'CustomOdmFilter', true]; + yield 'Generate ODM filter not interactively' => ['odm', 'CustomOdmFilter', false]; + } + + #[DataProvider('filterErrorProvider')] + public function testCommandFailsWithInvalidInput(array $inputs, string $exceptionClass = InvalidArgumentException::class): void + { + $this->expectException($exceptionClass); + $command = (new Application(self::bootKernel()))->find('make:filter'); + (new CommandTester($command))->execute($inputs); + } + + public static function filterErrorProvider(): \Generator + { + yield 'Missing type and name arguments' => [ + [], + MissingInputException::class, + ]; + + yield 'Invalid type argument' => [ + ['type' => 'john', 'name' => 'MyCustomFilter'], + ]; + + yield 'No valid type argument given' => [ + ['type' => 'John', 'name' => 'MyCustomFilter'], + ]; + + yield 'Missing name argument' => [ + ['type' => 'orm'], + MissingInputException::class, + ]; + } + + private static function tempDir(): string + { + return __DIR__.'/../../Fixtures/app/var/tmp'; + } + + private static function tempFile(string $path): string + { + return \sprintf('%s/%s', self::tempDir(), $path); + } +}