Skip to content

Commit ff248ae

Browse files
authored
Handle binary UUID in SearchFilter (#3774)
1 parent 0e3f820 commit ff248ae

File tree

12 files changed

+229
-35
lines changed

12 files changed

+229
-35
lines changed

.github/workflows/ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -384,7 +384,7 @@ jobs:
384384
- name: Clear test app cache
385385
run: tests/Fixtures/app/console cache:clear --ansi
386386
- name: Run Behat tests
387-
run: vendor/bin/behat --out=std --format=progress --profile=default --no-interaction
387+
run: vendor/bin/behat --out=std --format=progress --profile=default --no-interaction --tags '~@!lowest'
388388

389389
postgresql:
390390
name: Behat (PHP ${{ matrix.php }}) (PostgreSQL)

CHANGELOG.md

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,15 @@
11
# Changelog
22

3-
# 2.6.4
4-
5-
* OpenApi: Fix missing 422 responses in the documentation (#4086)
6-
73
## 2.6.3
84

95
* Identifiers: Re-allow `POST` operations even if no identifier is defined (#4052)
106
* Hydra: Fix partial pagination which no longer returns the `hydra:next` property (#4015)
117
* Security: Use a `NullToken` when using the new authenticator manager in the resource access checker (#4067)
128
* Mercure: Do not use data in options when deleting (#4056)
13-
* Doctrine: Support for foreign identifiers
14-
* JSON Schema: Allow generating documentation when property and method start from "is" (property `isActive` and method `isActive`)
9+
* Doctrine: Support for foreign identifiers (#4042)
10+
* Doctrine: Support for binary UUID in search filter (#3774)
11+
* JSON Schema: Allow generating documentation when property and method start from "is" (property `isActive` and method `isActive`) (#4064)
12+
* OpenAPI: Fix missing 422 responses in the documentation (#4086)
1513
* OpenAPI: Fix error when schema is empty (#4051)
1614
* OpenAPI: Do not set scheme to oauth2 when generating securitySchemes (#4073)
1715
* OpenAPI: Fix missing `$ref` when no `type` is used in context (#4076)

behat.yml.dist

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ postgres:
5050
- 'Behat\MinkExtension\Context\MinkContext'
5151
- 'behatch:context:rest'
5252
filters:
53-
tags: '~@sqlite&&~@mongodb&&~@elasticsearch'
53+
tags: '~@sqlite&&~@mongodb&&~@elasticsearch&&~@!postgres'
5454

5555
mongodb:
5656
suites:

features/doctrine/search_filter.feature

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -539,6 +539,52 @@ Feature: Search filter on collections
539539
}
540540
"""
541541

542+
@!postgres
543+
@!mongodb
544+
@!lowest
545+
Scenario: Search collection by binary UUID (Ramsey)
546+
Given there is a ramsey identified resource with binary uuid "c19900a9-d2b2-45bf-b040-05c72d321282"
547+
And there is a ramsey identified resource with binary uuid "a96cb2ed-e3dc-4449-9842-830e770cdecc"
548+
When I send a "GET" request to "/ramsey_uuid_binary_dummies?id=c19900a9-d2b2-45bf-b040-05c72d321282"
549+
Then the response status code should be 200
550+
And the response should be in JSON
551+
And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
552+
And the JSON node "hydra:totalItems" should be equal to "1"
553+
554+
@!postgres
555+
@!mongodb
556+
@!lowest
557+
Scenario: Search collection by binary UUID (Ramsey) (multiple values)
558+
Given there is a ramsey identified resource with binary uuid "f71a6469-1bfc-4945-bad1-d6092f09a8c3"
559+
When I send a "GET" request to "/ramsey_uuid_binary_dummies?id[]=c19900a9-d2b2-45bf-b040-05c72d321282&id[]=f71a6469-1bfc-4945-bad1-d6092f09a8c3"
560+
Then the response status code should be 200
561+
And the response should be in JSON
562+
And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
563+
And the JSON node "hydra:totalItems" should be equal to "2"
564+
565+
@!postgres
566+
@!mongodb
567+
@!lowest
568+
Scenario: Search collection by related binary UUID (Ramsey)
569+
Given there is a ramsey identified resource with binary uuid "56fa36c3-2b5e-4813-9e3a-b0bbe2ab5553" having a related resource with binary uuid "02227dc6-a371-4b8b-a34c-bbbf921b8ebd"
570+
And there is a ramsey identified resource with binary uuid "4d796212-4b26-4e19-b092-a32d990b1e7e" having a related resource with binary uuid "31f64c33-6061-4fc1-b0e8-f4711b607c7d"
571+
When I send a "GET" request to "/ramsey_uuid_binary_dummies?relateds=02227dc6-a371-4b8b-a34c-bbbf921b8ebd"
572+
Then the response status code should be 200
573+
And the response should be in JSON
574+
And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
575+
And the JSON node "hydra:totalItems" should be equal to "1"
576+
577+
@!postgres
578+
@!mongodb
579+
@!lowest
580+
Scenario: Search collection by related binary UUID (Ramsey) (multiple values)
581+
Given there is a ramsey identified resource with binary uuid "3248c908-a89d-483a-b75f-25888730d391" having a related resource with binary uuid "d7b2e909-37b0-411e-814c-74e044afbccb"
582+
When I send a "GET" request to "/ramsey_uuid_binary_dummies?relateds[]=02227dc6-a371-4b8b-a34c-bbbf921b8ebd&relateds[]=d7b2e909-37b0-411e-814c-74e044afbccb"
583+
Then the response status code should be 200
584+
And the response should be in JSON
585+
And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
586+
And the JSON node "hydra:totalItems" should be equal to "2"
587+
542588
Scenario: Search for entities within an impossible range
543589
When I send a "GET" request to "/dummies?name=MuYm"
544590
Then the response status code should be 200

src/Bridge/Doctrine/Orm/Filter/SearchFilter.php

Lines changed: 43 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,10 @@
2121
use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryNameGeneratorInterface;
2222
use ApiPlatform\Core\Exception\InvalidArgumentException;
2323
use Doctrine\DBAL\Types\Type as DBALType;
24+
use Doctrine\ORM\Query\Parameter;
2425
use Doctrine\ORM\QueryBuilder;
2526
use Doctrine\Persistence\ManagerRegistry;
27+
use Doctrine\Persistence\Mapping\ClassMetadata;
2628
use Psr\Log\LoggerInterface;
2729
use Symfony\Component\HttpFoundation\RequestStack;
2830
use Symfony\Component\PropertyAccess\PropertyAccess;
@@ -113,7 +115,7 @@ protected function filterProperty(string $property, $value, QueryBuilder $queryB
113115
$caseSensitive = false;
114116
}
115117

116-
$this->addWhereByStrategy($strategy, $queryBuilder, $queryNameGenerator, $alias, $field, $values, $caseSensitive);
118+
$this->addWhereByStrategy($strategy, $queryBuilder, $queryNameGenerator, $alias, $field, $values, $caseSensitive, $metadata);
117119

118120
return;
119121
}
@@ -149,24 +151,44 @@ protected function filterProperty(string $property, $value, QueryBuilder $queryB
149151
$associationField = $associationFieldIdentifier;
150152
}
151153

154+
$type = $metadata->getTypeOfField($associationField);
155+
152156
if (1 === \count($values)) {
153157
$queryBuilder
154158
->andWhere($queryBuilder->expr()->eq($associationAlias.'.'.$associationField, ':'.$valueParameter))
155-
->setParameter($valueParameter, $values[0]);
156-
} else {
157-
$queryBuilder
158-
->andWhere($queryBuilder->expr()->in($associationAlias.'.'.$associationField, ':'.$valueParameter))
159-
->setParameter($valueParameter, $values);
159+
->setParameter($valueParameter, $values[0], $type);
160+
161+
return;
162+
}
163+
164+
$parameters = $queryBuilder->getParameters();
165+
$inQuery = [];
166+
167+
foreach ($values as $val) {
168+
$inQuery[] = ':'.$valueParameter;
169+
$parameters->add(new Parameter($valueParameter, $val, $type));
170+
$valueParameter = $queryNameGenerator->generateParameterName($associationField);
160171
}
172+
173+
$queryBuilder
174+
->andWhere($associationAlias.'.'.$associationField.' IN ('.implode(', ', $inQuery).')')
175+
->setParameters($parameters);
161176
}
162177

163178
/**
164179
* Adds where clause according to the strategy.
165180
*
166181
* @throws InvalidArgumentException If strategy does not exist
167182
*/
168-
protected function addWhereByStrategy(string $strategy, QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $alias, string $field, $values, bool $caseSensitive)
183+
protected function addWhereByStrategy(string $strategy, QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $alias, string $field, $values, bool $caseSensitive/*, ClassMetadata $metadata*/)
169184
{
185+
if (\func_num_args() > 7 && ($metadata = func_get_arg(7)) instanceof ClassMetadata) {
186+
$type = $metadata->getTypeOfField($field);
187+
} else {
188+
@trigger_error(sprintf('Method %s() will have a 8th argument `$metadata` in version API Platform 3.0.', __FUNCTION__), \E_USER_DEPRECATED);
189+
$type = null;
190+
}
191+
170192
if (!\is_array($values)) {
171193
$values = [$values];
172194
}
@@ -175,18 +197,26 @@ protected function addWhereByStrategy(string $strategy, QueryBuilder $queryBuild
175197
$valueParameter = ':'.$queryNameGenerator->generateParameterName($field);
176198
$aliasedField = sprintf('%s.%s', $alias, $field);
177199

178-
if (null == $strategy || self::STRATEGY_EXACT == $strategy) {
179-
if (1 == \count($values)) {
200+
if (self::STRATEGY_EXACT === $strategy) {
201+
if (1 === \count($values)) {
180202
$queryBuilder
181203
->andWhere($queryBuilder->expr()->eq($wrapCase($aliasedField), $wrapCase($valueParameter)))
182-
->setParameter($valueParameter, $values[0]);
204+
->setParameter($valueParameter, $values[0], $type);
183205

184206
return;
185207
}
186208

209+
$parameters = $queryBuilder->getParameters();
210+
$inQuery = [];
211+
foreach ($values as $value) {
212+
$inQuery[] = $valueParameter;
213+
$parameters->add(new Parameter($valueParameter, $caseSensitive ? $value : strtolower($value), $type));
214+
$valueParameter = ':'.$queryNameGenerator->generateParameterName($field);
215+
}
216+
187217
$queryBuilder
188-
->andWhere($queryBuilder->expr()->in($wrapCase($aliasedField), $valueParameter))
189-
->setParameter($valueParameter, $caseSensitive ? $values : array_map('strtolower', $values));
218+
->andWhere($wrapCase($aliasedField).' IN ('.implode(', ', $inQuery).')')
219+
->setParameters($parameters);
190220

191221
return;
192222
}
@@ -228,7 +258,7 @@ protected function addWhereByStrategy(string $strategy, QueryBuilder $queryBuild
228258
}
229259

230260
$queryBuilder->andWhere($queryBuilder->expr()->orX(...$ors));
231-
array_walk($parameters, [$queryBuilder, 'setParameter']);
261+
array_walk($parameters, [$queryBuilder, 'setParameter'], $type);
232262
}
233263

234264
/**

tests/Behat/DoctrineContext.php

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,7 @@
139139
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\Pet;
140140
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\Product;
141141
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\Question;
142+
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\RamseyUuidBinaryDummy;
142143
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\RamseyUuidDummy;
143144
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\RelatedDummy;
144145
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\RelatedOwnedDummy;
@@ -1306,6 +1307,35 @@ public function thereIsARamseyIdentifiedResource(string $uuid)
13061307
$this->manager->flush();
13071308
}
13081309

1310+
/**
1311+
* @Given there is a ramsey identified resource with binary uuid :uuid
1312+
*/
1313+
public function thereIsARamseyIdentifiedResourceWithBinaryUuid(string $uuid)
1314+
{
1315+
$dummy = new RamseyUuidBinaryDummy();
1316+
$dummy->setId($uuid);
1317+
1318+
$this->manager->persist($dummy);
1319+
$this->manager->flush();
1320+
}
1321+
1322+
/**
1323+
* @Given there is a ramsey identified resource with binary uuid :uuid having a related resource with binary uuid :uuid_related
1324+
*/
1325+
public function thereIsARamseyIdentifiedResourceWithBinaryUuidHavingARelatedResourceWithBinaryUuid(string $uuid, string $uuidRelated)
1326+
{
1327+
$related = new RamseyUuidBinaryDummy();
1328+
$related->setId($uuidRelated);
1329+
1330+
$dummy = new RamseyUuidBinaryDummy();
1331+
$dummy->setId($uuid);
1332+
$dummy->addRelated($related);
1333+
1334+
$this->manager->persist($related);
1335+
$this->manager->persist($dummy);
1336+
$this->manager->flush();
1337+
}
1338+
13091339
/**
13101340
* @Given there is a dummy object with a fourth level relation
13111341
*/

tests/Bridge/Doctrine/Orm/Filter/SearchFilterTest.php

Lines changed: 10 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -390,22 +390,18 @@ public function provideApplyTestData(): array
390390
$filterFactory,
391391
],
392392
'exact (multiple values)' => [
393-
sprintf('SELECT %s FROM %s %1$s WHERE %1$s.name IN(:name_p1)', $this->alias, Dummy::class),
393+
sprintf('SELECT %s FROM %s %1$s WHERE %1$s.name IN (:name_p1, :name_p2)', $this->alias, Dummy::class),
394394
[
395-
'name_p1' => [
396-
'CaSE',
397-
'SENSitive',
398-
],
395+
'name_p1' => 'CaSE',
396+
'name_p2' => 'SENSitive',
399397
],
400398
$filterFactory,
401399
],
402400
'exact (multiple values; case insensitive)' => [
403-
sprintf('SELECT %s FROM %s %1$s WHERE LOWER(%1$s.name) IN(:name_p1)', $this->alias, Dummy::class),
401+
sprintf('SELECT %s FROM %s %1$s WHERE LOWER(%1$s.name) IN (:name_p1, :name_p2)', $this->alias, Dummy::class),
404402
[
405-
'name_p1' => [
406-
'case',
407-
'insensitive',
408-
],
403+
'name_p1' => 'case',
404+
'name_p2' => 'insensitive',
409405
],
410406
$filterFactory,
411407
],
@@ -547,10 +543,11 @@ public function provideApplyTestData(): array
547543
$filterFactory,
548544
],
549545
'mixed IRI and entity ID values for relations' => [
550-
sprintf('SELECT %s FROM %s %1$s INNER JOIN %1$s.relatedDummies relatedDummies_a1 WHERE %1$s.relatedDummy IN(:relatedDummy_p1) AND relatedDummies_a1.id = :relatedDummies_p2', $this->alias, Dummy::class),
546+
sprintf('SELECT %s FROM %s %1$s INNER JOIN %1$s.relatedDummies relatedDummies_a1 WHERE %1$s.relatedDummy IN (:relatedDummy_p1, :relatedDummy_p2) AND relatedDummies_a1.id = :relatedDummies_p4', $this->alias, Dummy::class),
551547
[
552-
'relatedDummy_p1' => [1, 2],
553-
'relatedDummies_p2' => 1,
548+
'relatedDummy_p1' => 1,
549+
'relatedDummy_p2' => 2,
550+
'relatedDummies_p4' => 1,
554551
],
555552
$filterFactory,
556553
],
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
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\Core\Tests\Fixtures\TestBundle\Entity;
15+
16+
use ApiPlatform\Core\Annotation\ApiFilter;
17+
use ApiPlatform\Core\Annotation\ApiResource;
18+
use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\SearchFilter;
19+
use Doctrine\Common\Collections\ArrayCollection;
20+
use Doctrine\Common\Collections\Collection;
21+
use Doctrine\ORM\Mapping as ORM;
22+
use Ramsey\Uuid\Uuid;
23+
use Ramsey\Uuid\UuidInterface;
24+
25+
/**
26+
* @ORM\Entity
27+
* @ApiResource
28+
* @ApiFilter(SearchFilter::class, properties={"id"="exact", "relateds"="exact"})
29+
*/
30+
class RamseyUuidBinaryDummy
31+
{
32+
/**
33+
* @var UuidInterface
34+
*
35+
* @ORM\Id
36+
* @ORM\Column(type="uuid_binary", unique=true)
37+
*/
38+
private $id;
39+
40+
/**
41+
* @var Collection<RamseyUuidBinaryDummy>
42+
*
43+
* @ORM\OneToMany(targetEntity="RamseyUuidBinaryDummy", mappedBy="relatedParent")
44+
*/
45+
private $relateds;
46+
47+
/**
48+
* @var ?RamseyUuidBinaryDummy
49+
*
50+
* @ORM\ManyToOne(targetEntity="RamseyUuidBinaryDummy", inversedBy="relateds")
51+
*/
52+
private $relatedParent;
53+
54+
public function __construct()
55+
{
56+
$this->relateds = new ArrayCollection();
57+
}
58+
59+
public function getId(): UuidInterface
60+
{
61+
return $this->id;
62+
}
63+
64+
public function setId(string $uuid): void
65+
{
66+
$this->id = Uuid::fromString($uuid);
67+
}
68+
69+
public function getRelateds(): Collection
70+
{
71+
return $this->relateds;
72+
}
73+
74+
public function addRelated(self $dummy): void
75+
{
76+
$this->relateds->add($dummy);
77+
$dummy->setRelatedParent($this);
78+
}
79+
80+
public function getRelatedParent(): ?self
81+
{
82+
return $this->relatedParent;
83+
}
84+
85+
public function setRelatedParent(self $dummy): void
86+
{
87+
$this->relatedParent = $dummy;
88+
}
89+
}

tests/Fixtures/app/config/config_common.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@ doctrine:
88
path: '%kernel.cache_dir%/db.sqlite'
99
charset: 'UTF8'
1010
types:
11-
uuid: Ramsey\Uuid\Doctrine\UuidType
11+
uuid: Ramsey\Uuid\Doctrine\UuidType
12+
uuid_binary: Ramsey\Uuid\Doctrine\UuidBinaryType
1213

1314
orm:
1415
auto_generate_proxy_classes: '%kernel.debug%'

tests/Fixtures/app/config/config_mysql.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,4 @@ doctrine:
1313
server_version: '%env(MYSQL_VERSION)%'
1414
types:
1515
uuid: Ramsey\Uuid\Doctrine\UuidType
16+
uuid_binary: Ramsey\Uuid\Doctrine\UuidBinaryType

0 commit comments

Comments
 (0)