diff --git a/src/Doctrine/Orm/Filter/ExactFilter.php b/src/Doctrine/Orm/Filter/ExactFilter.php index bd45dbf5d20..f7fbf4cf54b 100644 --- a/src/Doctrine/Orm/Filter/ExactFilter.php +++ b/src/Doctrine/Orm/Filter/ExactFilter.php @@ -36,6 +36,11 @@ public function apply(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $q $parameter = $context['parameter']; $value = $parameter->getValue(); + // associative arrays are operator-maps owned by ComparisonFilter/DateFilter, not equality + if (\is_array($value) && !array_is_list($value)) { + return; + } + if (null === $parameter->getProperty()) { throw new InvalidArgumentException(\sprintf('The filter parameter with key "%s" must specify a property. Please provide the property explicitly.', $parameter->getKey())); } diff --git a/src/Doctrine/Orm/Filter/PartialSearchFilter.php b/src/Doctrine/Orm/Filter/PartialSearchFilter.php index 2b78b5350f5..07e2103edbc 100644 --- a/src/Doctrine/Orm/Filter/PartialSearchFilter.php +++ b/src/Doctrine/Orm/Filter/PartialSearchFilter.php @@ -49,6 +49,11 @@ public function apply(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $q $field = $alias.'.'.$property; $values = $parameter->getValue(); + // associative arrays are operator-maps owned by ComparisonFilter/DateFilter, not equality + if (\is_array($values) && !array_is_list($values)) { + return; + } + if (!is_iterable($values)) { $parameterName = $queryNameGenerator->generateParameterName($property); $queryBuilder->setParameter($parameterName, $this->formatLikeValue($values)); diff --git a/tests/Fixtures/TestBundle/Entity/ExactAndComparisonParameter.php b/tests/Fixtures/TestBundle/Entity/ExactAndComparisonParameter.php new file mode 100644 index 00000000000..56508b8487e --- /dev/null +++ b/tests/Fixtures/TestBundle/Entity/ExactAndComparisonParameter.php @@ -0,0 +1,57 @@ + + * + * 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\ComparisonFilter; +use ApiPlatform\Doctrine\Orm\Filter\ExactFilter; +use ApiPlatform\Metadata\ApiResource; +use ApiPlatform\Metadata\GetCollection; +use ApiPlatform\Metadata\QueryParameter; +use ApiPlatform\Tests\Fixtures\TestBundle\Parameter\OperatorMapQueryParameter; +use Doctrine\ORM\Mapping as ORM; + +#[ApiResource(openapi: false)] +#[GetCollection( + uriTemplate: 'exact_and_comparison_parameter{._format}', + parameters: [ + 'quantity' => new QueryParameter(filter: new ExactFilter(), property: 'quantity'), + 'quantityComparison' => new OperatorMapQueryParameter(key: 'quantity', filter: new ComparisonFilter(new ExactFilter()), property: 'quantity'), + ] +)] +#[ORM\Entity] +class ExactAndComparisonParameter +{ + #[ORM\Column(type: 'integer')] + #[ORM\Id] + #[ORM\GeneratedValue(strategy: 'AUTO')] + private ?int $id = null; + + #[ORM\Column(type: 'integer')] + private int $quantity = 0; + + public function getId(): ?int + { + return $this->id; + } + + public function getQuantity(): int + { + return $this->quantity; + } + + public function setQuantity(int $quantity): void + { + $this->quantity = $quantity; + } +} diff --git a/tests/Fixtures/TestBundle/Parameter/OperatorMapQueryParameter.php b/tests/Fixtures/TestBundle/Parameter/OperatorMapQueryParameter.php new file mode 100644 index 00000000000..a24a7a32498 --- /dev/null +++ b/tests/Fixtures/TestBundle/Parameter/OperatorMapQueryParameter.php @@ -0,0 +1,24 @@ + + * + * 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\Parameter; + +use ApiPlatform\Metadata\QueryParameter; + +/** + * Distinct class so an operator-map filter can share an HTTP key with a scalar filter: + * Parameters dedup by (key, parameter class), so two parameters on the same key must differ in class. + */ +final class OperatorMapQueryParameter extends QueryParameter +{ +} diff --git a/tests/Functional/Parameters/DoctrineTest.php b/tests/Functional/Parameters/DoctrineTest.php index abce8d43447..dc4c89f27c4 100644 --- a/tests/Functional/Parameters/DoctrineTest.php +++ b/tests/Functional/Parameters/DoctrineTest.php @@ -17,6 +17,7 @@ use ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\FilterWithStateOptions; use ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\FilterWithStateOptionsAndNoApiFilter; use ApiPlatform\Tests\Fixtures\TestBundle\Document\SearchFilterParameter as SearchFilterParameterDocument; +use ApiPlatform\Tests\Fixtures\TestBundle\Entity\ExactAndComparisonParameter; use ApiPlatform\Tests\Fixtures\TestBundle\Entity\FilterWithStateOptionsAndNoApiFilterEntity; use ApiPlatform\Tests\Fixtures\TestBundle\Entity\FilterWithStateOptionsEntity; use ApiPlatform\Tests\Fixtures\TestBundle\Entity\ProductWithQueryParameter; @@ -42,9 +43,46 @@ public static function getResources(): array FilterWithStateOptions::class, FilterWithStateOptionsAndNoApiFilter::class, ProductWithQueryParameter::class, + ExactAndComparisonParameter::class, ]; } + public function testExactFilterIgnoresOperatorMap(): void + { + if ($this->isMongoDB()) { + $this->markTestSkipped('Not tested with mongodb.'); + } + + $resource = ExactAndComparisonParameter::class; + $this->recreateSchema([$resource]); + + $container = static::$kernel->getContainer(); + $manager = $container->get('doctrine')->getManager(); + foreach ([5, 8, 10, 15] as $q) { + $e = new ExactAndComparisonParameter(); + $e->setQuantity($q); + $manager->persist($e); + } + $manager->flush(); + + $route = 'exact_and_comparison_parameter'; + + // Exact match: ?quantity=10 must return only the row with quantity = 10. + $response = self::createClient()->request('GET', $route.'?quantity=10'); + $this->assertResponseIsSuccessful(); + $members = $response->toArray()['hydra:member']; + $this->assertCount(1, $members); + $this->assertSame(10, $members[0]['quantity']); + + // Operator map: ?quantity[lt]=10 must apply only the comparison (< 10), + // the exact filter must NOT also inject `quantity IN ('lt' => ...)`. + $response = self::createClient()->request('GET', $route.'?quantity[lt]=10'); + $this->assertResponseIsSuccessful(); + $quantities = array_map(static fn ($m) => $m['quantity'], $response->toArray()['hydra:member']); + sort($quantities); + $this->assertSame([5, 8], $quantities); + } + public function testDoctrineEntitySearchFilter(): void { $resource = $this->isMongoDB() ? SearchFilterParameterDocument::class : SearchFilterParameter::class;