From a5fe140610c8bd275b5a41321628fdd40af64374 Mon Sep 17 00:00:00 2001 From: Vincent Amstoutz Date: Wed, 23 Oct 2024 17:15:02 +0200 Subject: [PATCH] feat(doctrine): boolean filter like laravel filters --- .../Common/Filter/BooleanFilterTrait.php | 4 +- .../Odm/Extension/ParameterExtension.php | 49 +++-- src/Doctrine/Odm/Filter/AbstractFilter.php | 33 +++- src/Doctrine/Odm/Filter/BooleanFilter.php | 12 +- .../ManagerRegistryConfigurableInterface.php | 21 +++ src/Doctrine/Odm/PropertyHelperTrait.php | 8 +- .../Orm/Extension/ParameterExtension.php | 40 +++- src/Doctrine/Orm/Filter/AbstractFilter.php | 34 +++- src/Doctrine/Orm/Filter/BooleanFilter.php | 12 +- .../ManagerRegistryConfigurableInterface.php | 21 +++ src/Doctrine/Orm/PropertyHelperTrait.php | 2 +- .../Resources/config/doctrine_mongodb_odm.xml | 2 +- .../Bundle/Resources/config/doctrine_orm.xml | 1 + .../Document/FilteredBooleanParameter.php | 60 ++++++ .../Entity/FilteredBooleanParameter.php | 62 +++++++ .../Parameters/BooleanFilterTest.php | 173 ++++++++++++++++++ 16 files changed, 493 insertions(+), 41 deletions(-) create mode 100644 src/Doctrine/Odm/Filter/ManagerRegistryConfigurableInterface.php create mode 100644 src/Doctrine/Orm/Filter/ManagerRegistryConfigurableInterface.php create mode 100644 tests/Fixtures/TestBundle/Document/FilteredBooleanParameter.php create mode 100644 tests/Fixtures/TestBundle/Entity/FilteredBooleanParameter.php create mode 100644 tests/Functional/Parameters/BooleanFilterTest.php diff --git a/src/Doctrine/Common/Filter/BooleanFilterTrait.php b/src/Doctrine/Common/Filter/BooleanFilterTrait.php index c9ee4a364d8..cbf8fd8f7c2 100644 --- a/src/Doctrine/Common/Filter/BooleanFilterTrait.php +++ b/src/Doctrine/Common/Filter/BooleanFilterTrait.php @@ -42,7 +42,7 @@ public function getDescription(string $resourceClass): array $description = []; $properties = $this->getProperties(); - if (null === $properties) { + if (null === $properties && $this->hasManagerRegistry()) { $properties = array_fill_keys($this->getClassMetadata($resourceClass)->getFieldNames(), null); } @@ -61,7 +61,7 @@ public function getDescription(string $resourceClass): array return $description; } - abstract protected function getProperties(): ?array; + abstract public function getProperties(): ?array; abstract protected function getLogger(): LoggerInterface; diff --git a/src/Doctrine/Odm/Extension/ParameterExtension.php b/src/Doctrine/Odm/Extension/ParameterExtension.php index 0191871c07f..f07fa325439 100644 --- a/src/Doctrine/Odm/Extension/ParameterExtension.php +++ b/src/Doctrine/Odm/Extension/ParameterExtension.php @@ -14,11 +14,16 @@ namespace ApiPlatform\Doctrine\Odm\Extension; use ApiPlatform\Doctrine\Common\ParameterValueExtractorTrait; +use ApiPlatform\Doctrine\Odm\Filter\AbstractFilter; use ApiPlatform\Doctrine\Odm\Filter\FilterInterface; +use ApiPlatform\Doctrine\Odm\Filter\ManagerRegistryConfigurableInterface; use ApiPlatform\Metadata\Operation; use ApiPlatform\State\ParameterNotFound; +use Doctrine\Bundle\MongoDBBundle\ManagerRegistry; use Doctrine\ODM\MongoDB\Aggregation\Builder; +use Psr\Container\ContainerExceptionInterface; use Psr\Container\ContainerInterface; +use Psr\Container\NotFoundExceptionInterface; /** * Reads operation parameters and execute its filter. @@ -29,14 +34,20 @@ final class ParameterExtension implements AggregationCollectionExtensionInterfac { use ParameterValueExtractorTrait; - public function __construct(private readonly ContainerInterface $filterLocator) - { + public function __construct( + private readonly ContainerInterface $filterLocator, + private readonly ?ManagerRegistry $managerRegistry = null, + ) { } + /** + * @throws ContainerExceptionInterface + * @throws NotFoundExceptionInterface + */ private function applyFilter(Builder $aggregationBuilder, ?string $resourceClass = null, ?Operation $operation = null, array &$context = []): void { foreach ($operation->getParameters() ?? [] as $parameter) { - if (!($v = $parameter->getValue()) || $v instanceof ParameterNotFound) { + if (null === ($v = $parameter->getValue()) || $v instanceof ParameterNotFound) { continue; } @@ -45,14 +56,30 @@ private function applyFilter(Builder $aggregationBuilder, ?string $resourceClass continue; } - $filter = $this->filterLocator->has($filterId) ? $this->filterLocator->get($filterId) : null; - if ($filter instanceof FilterInterface) { - $filterContext = ['filters' => $values, 'parameter' => $parameter]; - $filter->apply($aggregationBuilder, $resourceClass, $operation, $filterContext); - // update by reference - if (isset($filterContext['mongodb_odm_sort_fields'])) { - $context['mongodb_odm_sort_fields'] = $filterContext['mongodb_odm_sort_fields']; - } + $filter = match (true) { + $filterId instanceof AbstractFilter => $filterId, + \is_string($filterId) && $this->filterLocator->has($filterId) => $this->filterLocator->get($filterId), + default => null, + }; + + if (!($filter instanceof FilterInterface)) { + return; + } + + if (!$filter->hasManagerRegistry() && $filter instanceof ManagerRegistryConfigurableInterface) { + $filter->setManagerRegistry($this->managerRegistry); + } + + if ([] === $filter->getProperties() || null === $filter->getProperties()) { + $key = $parameter->getProperty() ?? $parameter->getKey(); + $filter->setProperties([$key => []]); + } + + $filterContext = ['filters' => $values, 'parameter' => $parameter]; + $filter->apply($aggregationBuilder, $resourceClass, $operation, $filterContext); + // update by reference + if (isset($filterContext['mongodb_odm_sort_fields'])) { + $context['mongodb_odm_sort_fields'] = $filterContext['mongodb_odm_sort_fields']; } } } diff --git a/src/Doctrine/Odm/Filter/AbstractFilter.php b/src/Doctrine/Odm/Filter/AbstractFilter.php index 87c30390c32..f800b892e8b 100644 --- a/src/Doctrine/Odm/Filter/AbstractFilter.php +++ b/src/Doctrine/Odm/Filter/AbstractFilter.php @@ -17,8 +17,8 @@ use ApiPlatform\Doctrine\Common\PropertyHelperTrait; use ApiPlatform\Doctrine\Odm\PropertyHelperTrait as MongoDbOdmPropertyHelperTrait; use ApiPlatform\Metadata\Operation; +use Doctrine\Bundle\MongoDBBundle\ManagerRegistry; use Doctrine\ODM\MongoDB\Aggregation\Builder; -use Doctrine\Persistence\ManagerRegistry; use Psr\Log\LoggerInterface; use Psr\Log\NullLogger; use Symfony\Component\Serializer\NameConverter\NameConverterInterface; @@ -30,14 +30,18 @@ * * @author Alan Poulain */ -abstract class AbstractFilter implements FilterInterface, PropertyAwareFilterInterface +abstract class AbstractFilter implements FilterInterface, PropertyAwareFilterInterface, ManagerRegistryConfigurableInterface { use MongoDbOdmPropertyHelperTrait; use PropertyHelperTrait; protected LoggerInterface $logger; - public function __construct(protected ManagerRegistry $managerRegistry, ?LoggerInterface $logger = null, protected ?array $properties = null, protected ?NameConverterInterface $nameConverter = null) - { + public function __construct( + protected ?ManagerRegistry $managerRegistry = null, + ?LoggerInterface $logger = null, + protected ?array $properties = null, + protected ?NameConverterInterface $nameConverter = null, + ) { $this->logger = $logger ?? new NullLogger(); } @@ -56,18 +60,35 @@ public function apply(Builder $aggregationBuilder, string $resourceClass, ?Opera */ abstract protected function filterProperty(string $property, $value, Builder $aggregationBuilder, string $resourceClass, ?Operation $operation = null, array &$context = []): void; + public function hasManagerRegistry(): bool + { + return $this->managerRegistry instanceof ManagerRegistry; + } + protected function getManagerRegistry(): ManagerRegistry { + if (!$this->hasManagerRegistry()) { + throw new \RuntimeException('ManagerRegistry must be initialized before accessing it.'); + } + return $this->managerRegistry; } - protected function getProperties(): ?array + public function setManagerRegistry(?ManagerRegistry $managerRegistry): ?ManagerRegistry + { + return $this->managerRegistry = $managerRegistry; + } + + /** + * @return array|null + */ + public function getProperties(): ?array { return $this->properties; } /** - * @param string[] $properties + * @param array $properties */ public function setProperties(array $properties): void { diff --git a/src/Doctrine/Odm/Filter/BooleanFilter.php b/src/Doctrine/Odm/Filter/BooleanFilter.php index babe3309ed0..19086725acd 100644 --- a/src/Doctrine/Odm/Filter/BooleanFilter.php +++ b/src/Doctrine/Odm/Filter/BooleanFilter.php @@ -14,7 +14,9 @@ namespace ApiPlatform\Doctrine\Odm\Filter; use ApiPlatform\Doctrine\Common\Filter\BooleanFilterTrait; +use ApiPlatform\Metadata\JsonSchemaFilterInterface; use ApiPlatform\Metadata\Operation; +use ApiPlatform\Metadata\Parameter; use Doctrine\ODM\MongoDB\Aggregation\Builder; use Doctrine\ODM\MongoDB\Types\Type as MongoDbType; @@ -104,7 +106,7 @@ * @author Teoh Han Hui * @author Alan Poulain */ -final class BooleanFilter extends AbstractFilter +final class BooleanFilter extends AbstractFilter implements JsonSchemaFilterInterface { use BooleanFilterTrait; @@ -139,4 +141,12 @@ protected function filterProperty(string $property, $value, Builder $aggregation $aggregationBuilder->match()->field($matchField)->equals($value); } + + /** + * @return array + */ + public function getSchema(Parameter $parameter): array + { + return $parameter->getSchema() ?? ['type' => 'boolean']; + } } diff --git a/src/Doctrine/Odm/Filter/ManagerRegistryConfigurableInterface.php b/src/Doctrine/Odm/Filter/ManagerRegistryConfigurableInterface.php new file mode 100644 index 00000000000..36f5767cff1 --- /dev/null +++ b/src/Doctrine/Odm/Filter/ManagerRegistryConfigurableInterface.php @@ -0,0 +1,21 @@ + + * + * 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\Doctrine\Odm\Filter; + +use Doctrine\Bundle\MongoDBBundle\ManagerRegistry; + +interface ManagerRegistryConfigurableInterface +{ + public function setManagerRegistry(?ManagerRegistry $managerRegistry): ?ManagerRegistry; +} diff --git a/src/Doctrine/Odm/PropertyHelperTrait.php b/src/Doctrine/Odm/PropertyHelperTrait.php index e1c7693f2b0..6e73db7893e 100644 --- a/src/Doctrine/Odm/PropertyHelperTrait.php +++ b/src/Doctrine/Odm/PropertyHelperTrait.php @@ -27,7 +27,7 @@ */ trait PropertyHelperTrait { - abstract protected function getManagerRegistry(): ManagerRegistry; + abstract protected function getManagerRegistry(): ?ManagerRegistry; /** * Splits the given property into parts. @@ -39,9 +39,9 @@ abstract protected function splitPropertyParts(string $property, string $resourc */ protected function getClassMetadata(string $resourceClass): ClassMetadata { - $manager = $this - ->getManagerRegistry() - ->getManagerForClass($resourceClass); + /** @var ?ManagerRegistry $managerRegistry */ + $managerRegistry = $this->getManagerRegistry(); + $manager = $managerRegistry?->getManagerForClass($resourceClass); if ($manager) { return $manager->getClassMetadata($resourceClass); diff --git a/src/Doctrine/Orm/Extension/ParameterExtension.php b/src/Doctrine/Orm/Extension/ParameterExtension.php index aa65efbe577..4ae6e0994a3 100644 --- a/src/Doctrine/Orm/Extension/ParameterExtension.php +++ b/src/Doctrine/Orm/Extension/ParameterExtension.php @@ -14,12 +14,17 @@ namespace ApiPlatform\Doctrine\Orm\Extension; use ApiPlatform\Doctrine\Common\ParameterValueExtractorTrait; +use ApiPlatform\Doctrine\Orm\Filter\AbstractFilter; use ApiPlatform\Doctrine\Orm\Filter\FilterInterface; +use ApiPlatform\Doctrine\Orm\Filter\ManagerRegistryConfigurableInterface; use ApiPlatform\Doctrine\Orm\Util\QueryNameGeneratorInterface; use ApiPlatform\Metadata\Operation; use ApiPlatform\State\ParameterNotFound; use Doctrine\ORM\QueryBuilder; +use Psr\Container\ContainerExceptionInterface; use Psr\Container\ContainerInterface; +use Psr\Container\NotFoundExceptionInterface; +use Symfony\Bridge\Doctrine\ManagerRegistry; /** * Reads operation parameters and execute its filter. @@ -30,17 +35,22 @@ final class ParameterExtension implements QueryCollectionExtensionInterface, Que { use ParameterValueExtractorTrait; - public function __construct(private readonly ContainerInterface $filterLocator) - { + public function __construct( + private readonly ContainerInterface $filterLocator, + private readonly ?ManagerRegistry $managerRegistry = null, + ) { } /** * @param array $context + * + * @throws ContainerExceptionInterface + * @throws NotFoundExceptionInterface */ private function applyFilter(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, ?Operation $operation = null, array $context = []): void { foreach ($operation?->getParameters() ?? [] as $parameter) { - if (!($v = $parameter->getValue()) || $v instanceof ParameterNotFound) { + if (null === ($v = $parameter->getValue()) || $v instanceof ParameterNotFound) { continue; } @@ -49,10 +59,28 @@ private function applyFilter(QueryBuilder $queryBuilder, QueryNameGeneratorInter continue; } - $filter = $this->filterLocator->has($filterId) ? $this->filterLocator->get($filterId) : null; - if ($filter instanceof FilterInterface) { - $filter->apply($queryBuilder, $queryNameGenerator, $resourceClass, $operation, ['filters' => $values, 'parameter' => $parameter] + $context); + $filter = match (true) { + $filterId instanceof AbstractFilter => $filterId, + \is_string($filterId) && $this->filterLocator->has($filterId) => $this->filterLocator->get($filterId), + default => null, + }; + + if (!($filter instanceof FilterInterface)) { + return; + } + + if (!$filter->hasManagerRegistry() && $filter instanceof ManagerRegistryConfigurableInterface) { + $filter->setManagerRegistry($this->managerRegistry); } + + if ([] === $filter->getProperties() || null === $filter->getProperties()) { + $key = $parameter->getProperty() ?? $parameter->getKey(); + $filter->setProperties([$key => []]); + } + + $filter->apply($queryBuilder, $queryNameGenerator, $resourceClass, $operation, + ['filters' => $values, 'parameter' => $parameter] + $context + ); } } diff --git a/src/Doctrine/Orm/Filter/AbstractFilter.php b/src/Doctrine/Orm/Filter/AbstractFilter.php index 4ec704638a7..179166c5493 100644 --- a/src/Doctrine/Orm/Filter/AbstractFilter.php +++ b/src/Doctrine/Orm/Filter/AbstractFilter.php @@ -24,14 +24,18 @@ use Psr\Log\NullLogger; use Symfony\Component\Serializer\NameConverter\NameConverterInterface; -abstract class AbstractFilter implements FilterInterface, PropertyAwareFilterInterface +abstract class AbstractFilter implements FilterInterface, PropertyAwareFilterInterface, ManagerRegistryConfigurableInterface { use OrmPropertyHelperTrait; use PropertyHelperTrait; protected LoggerInterface $logger; - public function __construct(protected ManagerRegistry $managerRegistry, ?LoggerInterface $logger = null, protected ?array $properties = null, protected ?NameConverterInterface $nameConverter = null) - { + public function __construct( + protected ?ManagerRegistry $managerRegistry = null, + ?LoggerInterface $logger = null, + protected ?array $properties = null, + protected ?NameConverterInterface $nameConverter = null, + ) { $this->logger = $logger ?? new NullLogger(); } @@ -53,29 +57,43 @@ public function apply(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $q */ abstract protected function filterProperty(string $property, $value, QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, ?Operation $operation = null, array $context = []): void; + public function hasManagerRegistry(): bool + { + return $this->managerRegistry instanceof ManagerRegistry; + } + protected function getManagerRegistry(): ManagerRegistry { + if (!$this->hasManagerRegistry()) { + throw new \RuntimeException('ManagerRegistry must be initialized before accessing it.'); + } + return $this->managerRegistry; } - protected function getProperties(): ?array + public function setManagerRegistry(?ManagerRegistry $managerRegistry): ?ManagerRegistry { - return $this->properties; + return $this->managerRegistry = $managerRegistry; } - protected function getLogger(): LoggerInterface + public function getProperties(): ?array { - return $this->logger; + return $this->properties; } /** - * @param string[] $properties + * @param array $properties */ public function setProperties(array $properties): void { $this->properties = $properties; } + protected function getLogger(): LoggerInterface + { + return $this->logger; + } + /** * Determines whether the given property is enabled. */ diff --git a/src/Doctrine/Orm/Filter/BooleanFilter.php b/src/Doctrine/Orm/Filter/BooleanFilter.php index e9f0a8373e0..a9ac1127a12 100644 --- a/src/Doctrine/Orm/Filter/BooleanFilter.php +++ b/src/Doctrine/Orm/Filter/BooleanFilter.php @@ -15,7 +15,9 @@ use ApiPlatform\Doctrine\Common\Filter\BooleanFilterTrait; use ApiPlatform\Doctrine\Orm\Util\QueryNameGeneratorInterface; +use ApiPlatform\Metadata\JsonSchemaFilterInterface; use ApiPlatform\Metadata\Operation; +use ApiPlatform\Metadata\Parameter; use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Query\Expr\Join; use Doctrine\ORM\QueryBuilder; @@ -106,7 +108,7 @@ * @author Amrouche Hamza * @author Teoh Han Hui */ -final class BooleanFilter extends AbstractFilter +final class BooleanFilter extends AbstractFilter implements JsonSchemaFilterInterface { use BooleanFilterTrait; @@ -145,4 +147,12 @@ protected function filterProperty(string $property, $value, QueryBuilder $queryB ->andWhere(\sprintf('%s.%s = :%s', $alias, $field, $valueParameter)) ->setParameter($valueParameter, $value); } + + /** + * @return array + */ + public function getSchema(Parameter $parameter): array + { + return $parameter->getSchema() ?? ['type' => 'boolean']; + } } diff --git a/src/Doctrine/Orm/Filter/ManagerRegistryConfigurableInterface.php b/src/Doctrine/Orm/Filter/ManagerRegistryConfigurableInterface.php new file mode 100644 index 00000000000..9404cf8ac8a --- /dev/null +++ b/src/Doctrine/Orm/Filter/ManagerRegistryConfigurableInterface.php @@ -0,0 +1,21 @@ + + * + * 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\Doctrine\Orm\Filter; + +use Doctrine\Persistence\ManagerRegistry; + +interface ManagerRegistryConfigurableInterface +{ + public function setManagerRegistry(?ManagerRegistry $managerRegistry): ?ManagerRegistry; +} diff --git a/src/Doctrine/Orm/PropertyHelperTrait.php b/src/Doctrine/Orm/PropertyHelperTrait.php index 8431e3e1680..d9376bc7ff6 100644 --- a/src/Doctrine/Orm/PropertyHelperTrait.php +++ b/src/Doctrine/Orm/PropertyHelperTrait.php @@ -29,7 +29,7 @@ */ trait PropertyHelperTrait { - abstract protected function getManagerRegistry(): ManagerRegistry; + abstract protected function getManagerRegistry(): ?ManagerRegistry; /** * Splits the given property into parts. diff --git a/src/Symfony/Bundle/Resources/config/doctrine_mongodb_odm.xml b/src/Symfony/Bundle/Resources/config/doctrine_mongodb_odm.xml index e4206ea097d..d6485bf1d1a 100644 --- a/src/Symfony/Bundle/Resources/config/doctrine_mongodb_odm.xml +++ b/src/Symfony/Bundle/Resources/config/doctrine_mongodb_odm.xml @@ -137,7 +137,7 @@ - + diff --git a/src/Symfony/Bundle/Resources/config/doctrine_orm.xml b/src/Symfony/Bundle/Resources/config/doctrine_orm.xml index d6b3b1ffee8..6e00d888829 100644 --- a/src/Symfony/Bundle/Resources/config/doctrine_orm.xml +++ b/src/Symfony/Bundle/Resources/config/doctrine_orm.xml @@ -150,6 +150,7 @@ + diff --git a/tests/Fixtures/TestBundle/Document/FilteredBooleanParameter.php b/tests/Fixtures/TestBundle/Document/FilteredBooleanParameter.php new file mode 100644 index 00000000000..efa2428f864 --- /dev/null +++ b/tests/Fixtures/TestBundle/Document/FilteredBooleanParameter.php @@ -0,0 +1,60 @@ + + * + * 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\Fixtures\TestBundle\Document; + +use ApiPlatform\Doctrine\Odm\Filter\BooleanFilter; +use ApiPlatform\Metadata\ApiResource; +use ApiPlatform\Metadata\GetCollection; +use ApiPlatform\Metadata\QueryParameter; +use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM; + +#[ApiResource] +#[GetCollection( + parameters: [ + 'active' => new QueryParameter( + filter: new BooleanFilter(), + ), + 'enabled' => new QueryParameter( + filter: new BooleanFilter(), + property: 'active', + ), + ], +)] +#[ODM\Document] +class FilteredBooleanParameter +{ + public function __construct( + #[ODM\Id(type: 'int', strategy: 'INCREMENT')] + public ?int $id = null, + + #[ODM\Field(type: 'bool', nullable: true)] + public ?bool $active = null, + ) { + } + + public function getId(): ?int + { + return $this->id; + } + + public function isActive(): bool + { + return $this->active; + } + + public function setActive(?bool $active): void + { + $this->active = $active; + } +} diff --git a/tests/Fixtures/TestBundle/Entity/FilteredBooleanParameter.php b/tests/Fixtures/TestBundle/Entity/FilteredBooleanParameter.php new file mode 100644 index 00000000000..6ba24650a79 --- /dev/null +++ b/tests/Fixtures/TestBundle/Entity/FilteredBooleanParameter.php @@ -0,0 +1,62 @@ + + * + * 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\Fixtures\TestBundle\Entity; + +use ApiPlatform\Doctrine\Orm\Filter\BooleanFilter; +use ApiPlatform\Metadata\ApiResource; +use ApiPlatform\Metadata\GetCollection; +use ApiPlatform\Metadata\QueryParameter; +use Doctrine\ORM\Mapping as ORM; + +#[ApiResource] +#[GetCollection( + parameters: [ + 'active' => new QueryParameter( + filter: new BooleanFilter(), + ), + 'enabled' => new QueryParameter( + filter: new BooleanFilter(), + property: 'active', + ), + ], +)] +#[ORM\Entity] +class FilteredBooleanParameter +{ + public function __construct( + #[ORM\Column] + #[ORM\Id] + #[ORM\GeneratedValue(strategy: 'AUTO')] + public ?int $id = null, + + #[ORM\Column(nullable: true)] + public ?bool $active = null, + ) { + } + + public function getId(): ?int + { + return $this->id; + } + + public function isActive(): bool + { + return $this->active; + } + + public function setActive(?bool $isActive): void + { + $this->active = $isActive; + } +} diff --git a/tests/Functional/Parameters/BooleanFilterTest.php b/tests/Functional/Parameters/BooleanFilterTest.php new file mode 100644 index 00000000000..d1a559d47f3 --- /dev/null +++ b/tests/Functional/Parameters/BooleanFilterTest.php @@ -0,0 +1,173 @@ + + * + * 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\Functional\Parameters; + +use ApiPlatform\Symfony\Bundle\Test\ApiTestCase; +use ApiPlatform\Tests\Fixtures\TestBundle\Document\FilteredBooleanParameter as FilteredBooleanParameterDocument; +use ApiPlatform\Tests\Fixtures\TestBundle\Entity\FilteredBooleanParameter; +use ApiPlatform\Tests\RecreateSchemaTrait; +use ApiPlatform\Tests\SetupClassResourcesTrait; +use Doctrine\ODM\MongoDB\MongoDBException; +use PHPUnit\Framework\Attributes\DataProvider; +use Symfony\Contracts\HttpClient\Exception\ClientExceptionInterface; +use Symfony\Contracts\HttpClient\Exception\DecodingExceptionInterface; +use Symfony\Contracts\HttpClient\Exception\RedirectionExceptionInterface; +use Symfony\Contracts\HttpClient\Exception\ServerExceptionInterface; +use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface; + +final class BooleanFilterTest extends ApiTestCase +{ + use RecreateSchemaTrait; + use SetupClassResourcesTrait; + + /** + * @return class-string[] + */ + public static function getResources(): array + { + return [FilteredBooleanParameter::class]; + } + + /** + * @throws MongoDBException + * @throws \Throwable + */ + protected function setUp(): void + { + $resource = $this->isMongoDB() ? FilteredBooleanParameterDocument::class : FilteredBooleanParameter::class; + + $this->recreateSchema([$resource]); + $this->loadFixtures($resource); + } + + /** + * @throws TransportExceptionInterface + * @throws ServerExceptionInterface + * @throws RedirectionExceptionInterface + * @throws DecodingExceptionInterface + * @throws ClientExceptionInterface + */ + #[DataProvider('validBooleanFilterProvider')] + public function testBooleanFilterWithValidValues(string $url, int $expectedCount): void + { + $response = self::createClient()->request('GET', $url); + $this->assertResponseIsSuccessful(); + + $data = $response->toArray(); + $entities = $data['hydra:member']; + + $this->assertCount($expectedCount, $entities, \sprintf('Expected %d items for URL %s', $expectedCount, $url)); + + $expectedValue = str_contains($url, '=true') || str_contains($url, '=1'); + foreach ($entities as $entity) { + $errorMessage = \sprintf("Expected 'active' to be %s", $expectedValue ? 'true' : 'false'); + $this->assertSame($expectedValue, $entity['active'], $errorMessage); + } + } + + public static function validBooleanFilterProvider(): \Generator + { + yield 'active_true' => ['/filtered_boolean_parameters?active=true', 2]; + yield 'active_false' => ['/filtered_boolean_parameters?active=false', 1]; + yield 'active_1' => ['/filtered_boolean_parameters?active=1', 2]; + yield 'active_0' => ['/filtered_boolean_parameters?active=0', 1]; + yield 'enabled_true' => ['/filtered_boolean_parameters?enabled=true', 2]; + yield 'enabled_false' => ['/filtered_boolean_parameters?enabled=false', 1]; + yield 'enabled_1' => ['/filtered_boolean_parameters?enabled=1', 2]; + yield 'enabled_0' => ['/filtered_boolean_parameters?enabled=0', 1]; + } + + /** + * @throws RedirectionExceptionInterface + * @throws DecodingExceptionInterface + * @throws ClientExceptionInterface + * @throws TransportExceptionInterface + * @throws ServerExceptionInterface + */ + #[DataProvider('nullValuesBooleanFilterProvider')] + public function testBooleanFilterWithNullValues(string $url): void + { + $response = self::createClient()->request('GET', $url); + $this->assertResponseIsSuccessful(); + + $data = $response->toArray(); + $entities = $data['hydra:member']; + + $expectedCount = 3; + $this->assertCount($expectedCount, $entities, \sprintf('Expected %d items for URL %s', $expectedCount, $url)); + } + + public static function nullValuesBooleanFilterProvider(): \Generator + { + yield 'null_value' => ['/filtered_boolean_parameters?active=null']; + yield 'null_value_alias' => ['/filtered_boolean_parameters?enabled=null']; + } + + /** + * @throws TransportExceptionInterface + * @throws RedirectionExceptionInterface + * @throws DecodingExceptionInterface + * @throws ClientExceptionInterface + * @throws ServerExceptionInterface + */ + #[DataProvider('invalidBooleanFilterProvider')] + public function testBooleanFilterWithInvalidValues(string $url): void + { + $response = self::createClient()->request('GET', $url); + + $this->assertEquals(422, $response->getStatusCode(), \sprintf('Expected status code 422 for URL %s', $url)); + $this->assertJsonContains([ + '@context' => '/contexts/ConstraintViolationList', + '@id' => '/validation_errors/c1051bb4-d103-4f74-8988-acbcafc7fdc3', + '@type' => 'ConstraintViolationList', + 'status' => 422, + 'violations' => [ + 0 => [ + 'propertyPath' => 'active', + 'message' => 'This value should not be blank.', + 'code' => 'c1051bb4-d103-4f74-8988-acbcafc7fdc3', + ], + ], + 'detail' => 'active: This value should not be blank.', + 'description' => 'active: This value should not be blank.', + 'type' => '/validation_errors/c1051bb4-d103-4f74-8988-acbcafc7fdc3', + 'title' => 'An error occurred', + 'hydra:description' => 'active: This value should not be blank.', + 'hydra:title' => 'An error occurred', + ]); + } + + public static function invalidBooleanFilterProvider(): \Generator + { + yield 'empty_value' => ['/filtered_boolean_parameters?active=']; + yield 'empty_value_alias' => ['/filtered_boolean_parameters?enabled=']; + } + + /** + * @throws \Throwable + * @throws MongoDBException + */ + private function loadFixtures(string $resource): void + { + $manager = $this->getManager(); + + $entitiesData = [true, true, false, null]; + foreach ($entitiesData as $activeValue) { + $entity = new $resource(active: $activeValue); + $manager->persist($entity); + } + + $manager->flush(); + } +}