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
5 changes: 5 additions & 0 deletions src/Symfony/Bundle/Resources/config/maker.xml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@
<argument type="service" id="api_platform.metadata.resource.name_collection_factory" />
<tag name="maker.command" />
</service>

<service id="api_platform.maker.command.filter" class="ApiPlatform\Symfony\Maker\MakeFilter">
<argument type="service" id="api_platform.metadata.resource.name_collection_factory" />
<tag name="maker.command" />
</service>
</services>

</container>
20 changes: 20 additions & 0 deletions src/Symfony/Maker/Enum/SupportedFilterTypes.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php

/*
* This file is part of the API Platform project.
*
* (c) Kévin Dunglas <[email protected]>
*
* 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';
}
102 changes: 102 additions & 0 deletions src/Symfony/Maker/MakeFilter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
<?php

/*
* This file is part of the API Platform project.
*
* (c) Kévin Dunglas <[email protected]>
*
* 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 (<fg=yellow>%s</>)', self::getFilterTypesAsString()))
->addArgument('name', InputArgument::REQUIRED, 'Choose a class name for your filter (e.g. <fg=yellow>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));
}
}
9 changes: 9 additions & 0 deletions src/Symfony/Maker/Resources/help/MakeFilter.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
The <info>%command.name%</info> command generates a new API Platform filter class for Doctrine ORM or ODM (MongoDB).

<info>php %command.full_name% type name</info>

<info>Important:</info>
- 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.

<info>Elasticsearch isn't supported yet.</info>
27 changes: 27 additions & 0 deletions src/Symfony/Maker/Resources/skeleton/OdmFilter.php.tpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?php declare(strict_types=1);
echo "<?php\n"; ?>

namespace <?php echo $namespace; ?>;

use ApiPlatform\Doctrine\Odm\Filter\FilterInterface;
use ApiPlatform\Metadata\BackwardCompatibleFilterDescriptionTrait;
use ApiPlatform\Metadata\Operation;
use Doctrine\ODM\MongoDB\Aggregation\Builder;

class <?php echo $class_name; ?> 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->
}
}
32 changes: 32 additions & 0 deletions src/Symfony/Maker/Resources/skeleton/OrmFilter.php.tpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?php declare(strict_types=1);
echo "<?php\n"; ?>

namespace <?php echo $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 <?php echo $class_name; ?> 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->
}
}
24 changes: 24 additions & 0 deletions tests/Fixtures/Symfony/Maker/CustomOdmFilter.fixture
Original file line number Diff line number Diff line change
@@ -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->
}
}
29 changes: 29 additions & 0 deletions tests/Fixtures/Symfony/Maker/CustomOrmFilter.fixture
Original file line number Diff line number Diff line change
@@ -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->
}
}
110 changes: 110 additions & 0 deletions tests/Symfony/Maker/MakeFilterTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
<?php

/*
* This file is part of the API Platform project.
*
* (c) Kévin Dunglas <[email protected]>
*
* 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);
}
}
Loading