Skip to content

Commit 05c51c2

Browse files
authored
fix(doctrine): ignore operator-map values in ExactFilter and PartialSearchFilter (#8415)
Fixes #8407
1 parent 400463f commit 05c51c2

5 files changed

Lines changed: 129 additions & 0 deletions

File tree

src/Doctrine/Orm/Filter/ExactFilter.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,11 @@ public function apply(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $q
3636
$parameter = $context['parameter'];
3737
$value = $parameter->getValue();
3838

39+
// associative arrays are operator-maps owned by ComparisonFilter/DateFilter, not equality
40+
if (\is_array($value) && !array_is_list($value)) {
41+
return;
42+
}
43+
3944
if (null === $parameter->getProperty()) {
4045
throw new InvalidArgumentException(\sprintf('The filter parameter with key "%s" must specify a property. Please provide the property explicitly.', $parameter->getKey()));
4146
}

src/Doctrine/Orm/Filter/PartialSearchFilter.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,11 @@ public function apply(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $q
4949
$field = $alias.'.'.$property;
5050
$values = $parameter->getValue();
5151

52+
// associative arrays are operator-maps owned by ComparisonFilter/DateFilter, not equality
53+
if (\is_array($values) && !array_is_list($values)) {
54+
return;
55+
}
56+
5257
if (!is_iterable($values)) {
5358
$parameterName = $queryNameGenerator->generateParameterName($property);
5459
$queryBuilder->setParameter($parameterName, $this->formatLikeValue($values));
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <dunglas@gmail.com>
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\Fixtures\TestBundle\Entity;
15+
16+
use ApiPlatform\Doctrine\Orm\Filter\ComparisonFilter;
17+
use ApiPlatform\Doctrine\Orm\Filter\ExactFilter;
18+
use ApiPlatform\Metadata\ApiResource;
19+
use ApiPlatform\Metadata\GetCollection;
20+
use ApiPlatform\Metadata\QueryParameter;
21+
use ApiPlatform\Tests\Fixtures\TestBundle\Parameter\OperatorMapQueryParameter;
22+
use Doctrine\ORM\Mapping as ORM;
23+
24+
#[ApiResource(openapi: false)]
25+
#[GetCollection(
26+
uriTemplate: 'exact_and_comparison_parameter{._format}',
27+
parameters: [
28+
'quantity' => new QueryParameter(filter: new ExactFilter(), property: 'quantity'),
29+
'quantityComparison' => new OperatorMapQueryParameter(key: 'quantity', filter: new ComparisonFilter(new ExactFilter()), property: 'quantity'),
30+
]
31+
)]
32+
#[ORM\Entity]
33+
class ExactAndComparisonParameter
34+
{
35+
#[ORM\Column(type: 'integer')]
36+
#[ORM\Id]
37+
#[ORM\GeneratedValue(strategy: 'AUTO')]
38+
private ?int $id = null;
39+
40+
#[ORM\Column(type: 'integer')]
41+
private int $quantity = 0;
42+
43+
public function getId(): ?int
44+
{
45+
return $this->id;
46+
}
47+
48+
public function getQuantity(): int
49+
{
50+
return $this->quantity;
51+
}
52+
53+
public function setQuantity(int $quantity): void
54+
{
55+
$this->quantity = $quantity;
56+
}
57+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <dunglas@gmail.com>
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\Fixtures\TestBundle\Parameter;
15+
16+
use ApiPlatform\Metadata\QueryParameter;
17+
18+
/**
19+
* Distinct class so an operator-map filter can share an HTTP key with a scalar filter:
20+
* Parameters dedup by (key, parameter class), so two parameters on the same key must differ in class.
21+
*/
22+
final class OperatorMapQueryParameter extends QueryParameter
23+
{
24+
}

tests/Functional/Parameters/DoctrineTest.php

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
use ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\FilterWithStateOptions;
1818
use ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\FilterWithStateOptionsAndNoApiFilter;
1919
use ApiPlatform\Tests\Fixtures\TestBundle\Document\SearchFilterParameter as SearchFilterParameterDocument;
20+
use ApiPlatform\Tests\Fixtures\TestBundle\Entity\ExactAndComparisonParameter;
2021
use ApiPlatform\Tests\Fixtures\TestBundle\Entity\FilterWithStateOptionsAndNoApiFilterEntity;
2122
use ApiPlatform\Tests\Fixtures\TestBundle\Entity\FilterWithStateOptionsEntity;
2223
use ApiPlatform\Tests\Fixtures\TestBundle\Entity\ProductWithQueryParameter;
@@ -42,9 +43,46 @@ public static function getResources(): array
4243
FilterWithStateOptions::class,
4344
FilterWithStateOptionsAndNoApiFilter::class,
4445
ProductWithQueryParameter::class,
46+
ExactAndComparisonParameter::class,
4547
];
4648
}
4749

50+
public function testExactFilterIgnoresOperatorMap(): void
51+
{
52+
if ($this->isMongoDB()) {
53+
$this->markTestSkipped('Not tested with mongodb.');
54+
}
55+
56+
$resource = ExactAndComparisonParameter::class;
57+
$this->recreateSchema([$resource]);
58+
59+
$container = static::$kernel->getContainer();
60+
$manager = $container->get('doctrine')->getManager();
61+
foreach ([5, 8, 10, 15] as $q) {
62+
$e = new ExactAndComparisonParameter();
63+
$e->setQuantity($q);
64+
$manager->persist($e);
65+
}
66+
$manager->flush();
67+
68+
$route = 'exact_and_comparison_parameter';
69+
70+
// Exact match: ?quantity=10 must return only the row with quantity = 10.
71+
$response = self::createClient()->request('GET', $route.'?quantity=10');
72+
$this->assertResponseIsSuccessful();
73+
$members = $response->toArray()['hydra:member'];
74+
$this->assertCount(1, $members);
75+
$this->assertSame(10, $members[0]['quantity']);
76+
77+
// Operator map: ?quantity[lt]=10 must apply only the comparison (< 10),
78+
// the exact filter must NOT also inject `quantity IN ('lt' => ...)`.
79+
$response = self::createClient()->request('GET', $route.'?quantity[lt]=10');
80+
$this->assertResponseIsSuccessful();
81+
$quantities = array_map(static fn ($m) => $m['quantity'], $response->toArray()['hydra:member']);
82+
sort($quantities);
83+
$this->assertSame([5, 8], $quantities);
84+
}
85+
4886
public function testDoctrineEntitySearchFilter(): void
4987
{
5088
$resource = $this->isMongoDB() ? SearchFilterParameterDocument::class : SearchFilterParameter::class;

0 commit comments

Comments
 (0)