Skip to content

Commit e6a80e1

Browse files
committed
feat(symfony): add make:filter command to generate API Platform filters
Allow to generate Doctrine ORM and MongoDB (ODM) filters
1 parent 26d2394 commit e6a80e1

File tree

9 files changed

+359
-0
lines changed

9 files changed

+359
-0
lines changed

src/Symfony/Bundle/Resources/config/maker.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,11 @@
1414
<argument type="service" id="api_platform.metadata.resource.name_collection_factory" />
1515
<tag name="maker.command" />
1616
</service>
17+
18+
<service id="api_platform.maker.command.filter" class="ApiPlatform\Symfony\Maker\MakeFilter">
19+
<argument type="service" id="api_platform.metadata.resource.name_collection_factory" />
20+
<tag name="maker.command" />
21+
</service>
1722
</services>
1823

1924
</container>
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace ApiPlatform\Symfony\Maker\Enum;
15+
16+
enum SupportedFilterTypes: string
17+
{
18+
case ORM = 'orm';
19+
case ODM = 'odm';
20+
}

src/Symfony/Maker/MakeFilter.php

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace ApiPlatform\Symfony\Maker;
15+
16+
use ApiPlatform\Metadata\Exception\InvalidArgumentException;
17+
use ApiPlatform\Symfony\Maker\Enum\SupportedFilterTypes;
18+
use Symfony\Bundle\MakerBundle\ConsoleStyle;
19+
use Symfony\Bundle\MakerBundle\DependencyBuilder;
20+
use Symfony\Bundle\MakerBundle\Generator;
21+
use Symfony\Bundle\MakerBundle\InputConfiguration;
22+
use Symfony\Bundle\MakerBundle\Maker\AbstractMaker;
23+
use Symfony\Component\Console\Command\Command;
24+
use Symfony\Component\Console\Input\InputArgument;
25+
use Symfony\Component\Console\Input\InputInterface;
26+
27+
final class MakeFilter extends AbstractMaker
28+
{
29+
/**
30+
* {@inheritdoc}
31+
*/
32+
public static function getCommandName(): string
33+
{
34+
return 'make:filter';
35+
}
36+
37+
/**
38+
* {@inheritdoc}
39+
*/
40+
public static function getCommandDescription(): string
41+
{
42+
return 'Creates an API Platform filter';
43+
}
44+
45+
/**
46+
* {@inheritdoc}
47+
*/
48+
public function configureCommand(Command $command, InputConfiguration $inputConfig): void
49+
{
50+
$command
51+
->addArgument('type', InputArgument::REQUIRED, \sprintf('Choose a type for your filter (<fg=yellow>%s</>)', self::getFilterTypesAsString()))
52+
->addArgument('name', InputArgument::REQUIRED, 'Choose a class name for your filter (e.g. <fg=yellow>AwesomeFilter</>)')
53+
->setHelp(file_get_contents(__DIR__.'/Resources/help/MakeFilter.txt'));
54+
}
55+
56+
/**
57+
* {@inheritdoc}
58+
*/
59+
public function configureDependencies(DependencyBuilder $dependencies): void
60+
{
61+
}
62+
63+
/**
64+
* {@inheritdoc}
65+
*
66+
* @throws \Exception
67+
*/
68+
public function generate(InputInterface $input, ConsoleStyle $io, Generator $generator): void
69+
{
70+
$typeArgument = strtolower((string) $input->getArgument('type'));
71+
$type = SupportedFilterTypes::tryFrom($typeArgument);
72+
if (null === $type) {
73+
throw new InvalidArgumentException(\sprintf('The type "%s" is not a valid filter type, valid options are: %s.', $typeArgument, self::getFilterTypesAsString()));
74+
}
75+
76+
$filterNameDetails = $generator->createClassNameDetails(
77+
name: $input->getArgument('name'),
78+
namespacePrefix: 'Filter\\'
79+
);
80+
$filterName = \sprintf('%sFilter', ucfirst($type->value));
81+
82+
$generator->generateClass(className: $filterNameDetails->getFullName(), templateName: \sprintf(
83+
'%s/Resources/skeleton/%s.php.tpl',
84+
__DIR__,
85+
$filterName
86+
));
87+
88+
$generator->writeChanges();
89+
90+
$this->writeSuccessMessage($io);
91+
$io->text([
92+
'Next: Open your filter class and start customizing it.',
93+
]);
94+
}
95+
96+
private static function getFilterTypesAsString(): string
97+
{
98+
$validOptions = array_column(SupportedFilterTypes::cases(), 'value');
99+
100+
return implode(' or ', array_map('strtoupper', $validOptions));
101+
}
102+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
The <info>%command.name%</info> command generates a new API Platform filter class for Doctrine ORM or ODM (MongoDB).
2+
3+
<info>php %command.full_name% type name</info>
4+
5+
<info>Important:</info>
6+
7+
- If you omit the argument, the command will prompt you to choose the filter type interactively.
8+
- If you omit the argument, the command will ask you to enter the class name interactively.
9+
10+
<info>Elasticsearch isn't supported yet.</info>
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<?php declare(strict_types=1);
2+
echo "<?php\n"; ?>
3+
4+
namespace <?php echo $namespace; ?>;
5+
6+
use ApiPlatform\Doctrine\Odm\Filter\FilterInterface;
7+
use ApiPlatform\Metadata\BackwardCompatibleFilterDescriptionTrait;
8+
use ApiPlatform\Metadata\Operation;
9+
use Doctrine\ODM\MongoDB\Aggregation\Builder;
10+
11+
class <?php echo $class_name; ?> implements FilterInterface
12+
{
13+
use BackwardCompatibleFilterDescriptionTrait; // Here for backward compatibility, keep it until 5.0.
14+
15+
public function apply(Builder $aggregationBuilder, string $resourceClass, ?Operation $operation = null, array &$context = []): void
16+
{
17+
// Retrieve the parameter and it's value
18+
// $parameter = $context['parameter'];
19+
// $value = $parameter->getValue();
20+
21+
// Retrieve the property
22+
// $property = $parameter->getProperty();
23+
24+
// TODO: make your awesome query using the $aggregationBuilder
25+
// $aggregationBuilder->
26+
}
27+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<?php declare(strict_types=1);
2+
echo "<?php\n"; ?>
3+
4+
namespace <?php echo $namespace; ?>;
5+
6+
use ApiPlatform\Doctrine\Orm\Filter\FilterInterface;
7+
use ApiPlatform\Doctrine\Orm\Util\QueryNameGeneratorInterface;
8+
use ApiPlatform\Metadata\BackwardCompatibleFilterDescriptionTrait;
9+
use ApiPlatform\Metadata\Operation;
10+
use Doctrine\ORM\QueryBuilder;
11+
12+
class <?php echo $class_name; ?> implements FilterInterface
13+
{
14+
use BackwardCompatibleFilterDescriptionTrait; // Here for backward compatibility, keep it until 5.0.
15+
16+
public function apply(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, ?Operation $operation = null, array $context = []): void
17+
{
18+
// Retrieve the parameter and it's value
19+
// $parameter = $context['parameter'];
20+
// $value = $parameter->getValue();
21+
22+
// Retrieve the property
23+
// $property = $parameter->getProperty();
24+
25+
// Retrieve alias and parameter name
26+
// $alias = $queryBuilder->getRootAliases()[0];
27+
// $parameterName = $queryNameGenerator->generateParameterName($property);
28+
29+
// TODO: make your awesome query using the $queryBuilder
30+
// $queryBuilder->
31+
}
32+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
namespace App\Filter;
2+
3+
use ApiPlatform\Doctrine\Odm\Filter\FilterInterface;
4+
use ApiPlatform\Metadata\BackwardCompatibleFilterDescriptionTrait;
5+
use ApiPlatform\Metadata\Operation;
6+
use Doctrine\ODM\MongoDB\Aggregation\Builder;
7+
8+
class CustomOdmFilter implements FilterInterface
9+
{
10+
use BackwardCompatibleFilterDescriptionTrait; // Here for backward compatibility, keep it until 5.0.
11+
12+
public function apply(Builder $aggregationBuilder, string $resourceClass, ?Operation $operation = null, array &$context = []): void
13+
{
14+
// Retrieve the parameter and it's value
15+
// $parameter = $context['parameter'];
16+
// $value = $parameter->getValue();
17+
18+
// Retrieve the property
19+
// $property = $parameter->getProperty();
20+
21+
// TODO: make your awesome query using the $aggregationBuilder
22+
// $aggregationBuilder->
23+
}
24+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
namespace App\Filter;
2+
3+
use ApiPlatform\Doctrine\Orm\Filter\FilterInterface;
4+
use ApiPlatform\Doctrine\Orm\Util\QueryNameGeneratorInterface;
5+
use ApiPlatform\Metadata\BackwardCompatibleFilterDescriptionTrait;
6+
use ApiPlatform\Metadata\Operation;
7+
use Doctrine\ORM\QueryBuilder;
8+
9+
class CustomOrmFilter implements FilterInterface
10+
{
11+
use BackwardCompatibleFilterDescriptionTrait; // Here for backward compatibility, keep it until 5.0.
12+
13+
public function apply(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, ?Operation $operation = null, array $context = []): void
14+
{
15+
// Retrieve the parameter and it's value
16+
// $parameter = $context['parameter'];
17+
// $value = $parameter->getValue();
18+
19+
// Retrieve the property
20+
// $property = $parameter->getProperty();
21+
22+
// Retrieve alias and parameter name
23+
// $alias = $queryBuilder->getRootAliases()[0];
24+
// $parameterName = $queryNameGenerator->generateParameterName($property);
25+
26+
// TODO: make your awesome query using the $queryBuilder
27+
// $queryBuilder->
28+
}
29+
}
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace ApiPlatform\Tests\Symfony\Maker;
15+
16+
use ApiPlatform\Metadata\Exception\InvalidArgumentException;
17+
use PHPUnit\Framework\Attributes\DataProvider;
18+
use Symfony\Bundle\FrameworkBundle\Console\Application;
19+
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
20+
use Symfony\Component\Console\Exception\MissingInputException;
21+
use Symfony\Component\Console\Tester\CommandTester;
22+
use Symfony\Component\Filesystem\Filesystem;
23+
24+
class MakeFilterTest extends KernelTestCase
25+
{
26+
protected function setup(): void
27+
{
28+
(new Filesystem())->remove(self::tempDir());
29+
}
30+
31+
#[DataProvider('filterProvider')]
32+
public function testMakeFilter(string $type, string $name, bool $isInteractive): void
33+
{
34+
$inputs = ['type' => $type, 'name' => $name];
35+
$newFilterFile = self::tempFile("src/Filter/{$name}.php");
36+
37+
$command = (new Application(self::bootKernel()))->find('make:filter');
38+
$commandTester = new CommandTester($command);
39+
$commandTester->setInputs($isInteractive ? $inputs : []);
40+
$commandTester->execute($isInteractive ? [] : $inputs);
41+
42+
$this->assertFileExists($newFilterFile);
43+
44+
$expected = preg_replace('~\R~u', "\r\n", file_get_contents(__DIR__."/../../Fixtures/Symfony/Maker/{$name}.fixture"));
45+
$result = preg_replace('~\R~u', "\r\n", file_get_contents($newFilterFile));
46+
$this->assertStringContainsString($expected, $result);
47+
48+
$display = $commandTester->getDisplay();
49+
$commandTester->assertCommandIsSuccessful();
50+
$interactiveOutputType = 'Choose a type for your filter';
51+
$interactiveOutputName = 'Choose a class name for your filter';
52+
53+
if ($isInteractive) {
54+
$this->assertStringContainsString($interactiveOutputType, $display);
55+
$this->assertStringContainsString($interactiveOutputName, $display);
56+
} else {
57+
$this->assertStringNotContainsString($interactiveOutputType, $display);
58+
$this->assertStringNotContainsString($interactiveOutputName, $display);
59+
}
60+
61+
$this->assertStringContainsString(' Next: Open your filter class and start customizing it.', $display);
62+
}
63+
64+
public static function filterProvider(): \Generator
65+
{
66+
yield 'Generate ORM filter' => ['orm', 'CustomOrmFilter', true];
67+
yield 'Generate ORM filter not interactively' => ['orm', 'CustomOrmFilter', false];
68+
yield 'Generate ODM filter' => ['odm', 'CustomOdmFilter', true];
69+
yield 'Generate ODM filter not interactively' => ['odm', 'CustomOdmFilter', false];
70+
}
71+
72+
#[DataProvider('filterErrorProvider')]
73+
public function testCommandFailsWithInvalidInput(array $inputs, string $exceptionClass = InvalidArgumentException::class): void
74+
{
75+
$this->expectException($exceptionClass);
76+
$command = (new Application(self::bootKernel()))->find('make:filter');
77+
(new CommandTester($command))->execute($inputs);
78+
}
79+
80+
public static function filterErrorProvider(): \Generator
81+
{
82+
yield 'Missing type and name arguments' => [
83+
[],
84+
MissingInputException::class,
85+
];
86+
87+
yield 'Invalid type argument' => [
88+
['type' => 'john', 'name' => 'MyCustomFilter'],
89+
];
90+
91+
yield 'No valid type argument given' => [
92+
['type' => 'John', 'name' => 'MyCustomFilter'],
93+
];
94+
95+
yield 'Missing name argument' => [
96+
['type' => 'orm'],
97+
MissingInputException::class,
98+
];
99+
}
100+
101+
private static function tempDir(): string
102+
{
103+
return __DIR__.'/../../Fixtures/app/var/tmp';
104+
}
105+
106+
private static function tempFile(string $path): string
107+
{
108+
return \sprintf('%s/%s', self::tempDir(), $path);
109+
}
110+
}

0 commit comments

Comments
 (0)