Skip to content

Commit f80b922

Browse files
committed
fix #7465 Add uuid filter
1 parent 34f3f1c commit f80b922

20 files changed

+1303
-1
lines changed
Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
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\Doctrine\Orm\Filter;
15+
16+
use ApiPlatform\Doctrine\Orm\Util\QueryBuilderHelper;
17+
use ApiPlatform\Doctrine\Orm\Util\QueryNameGeneratorInterface;
18+
use ApiPlatform\Metadata\BackwardCompatibleFilterDescriptionTrait;
19+
use ApiPlatform\Metadata\Exception\InvalidArgumentException;
20+
use ApiPlatform\Metadata\OpenApiParameterFilterInterface;
21+
use ApiPlatform\Metadata\Operation;
22+
use ApiPlatform\Metadata\Parameter;
23+
use ApiPlatform\Metadata\QueryParameter;
24+
use ApiPlatform\OpenApi\Model\Parameter as OpenApiParameter;
25+
use Doctrine\DBAL\ArrayParameterType;
26+
use Doctrine\DBAL\ParameterType;
27+
use Doctrine\DBAL\Types\ConversionException;
28+
use Doctrine\DBAL\Types\Type;
29+
use Doctrine\ORM\Query\Expr\Join;
30+
use Doctrine\ORM\QueryBuilder;
31+
32+
/**
33+
* @internal
34+
*/
35+
class AbstractUuidFilter extends AbstractFilter implements OpenApiParameterFilterInterface
36+
{
37+
use BackwardCompatibleFilterDescriptionTrait;
38+
39+
private const UUID_SCHEMA = [
40+
'type' => 'string',
41+
'format' => 'uuid',
42+
];
43+
44+
protected function filterProperty(string $property, mixed $value, QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, ?Operation $operation = null, array $context = []): void
45+
{
46+
if (
47+
null === $value
48+
|| !$this->isPropertyEnabled($property, $resourceClass)
49+
|| !$this->isPropertyMapped($property, $resourceClass, true)
50+
) {
51+
return;
52+
}
53+
54+
$alias = $queryBuilder->getRootAliases()[0];
55+
$field = $property;
56+
57+
$values = $this->normalizeValues((array) $value, $property);
58+
if (null === $values) {
59+
return;
60+
}
61+
62+
$associations = [];
63+
if ($this->isPropertyNested($property, $resourceClass)) {
64+
[$alias, $field, $associations] = $this->addJoinsForNestedProperty($property, $alias, $queryBuilder, $queryNameGenerator, $resourceClass, Join::INNER_JOIN);
65+
}
66+
67+
$metadata = $this->getNestedMetadata($resourceClass, $associations);
68+
69+
if ($metadata->hasField($field)) {
70+
$values = $this->convertValuesToTheDatabaseRepresentationIfNecessary($queryBuilder, $this->getDoctrineFieldType($property, $resourceClass), $values);
71+
$this->addWhere($queryBuilder, $queryNameGenerator, $alias, $field, $values);
72+
73+
return;
74+
}
75+
76+
// metadata doesn't have the field, nor an association on the field
77+
if (!$metadata->hasAssociation($field)) {
78+
return;
79+
}
80+
81+
// association, let's fetch the entity (or reference to it) if we can so we can make sure we get its orm id
82+
$associationResourceClass = $metadata->getAssociationTargetClass($field);
83+
$associationMetadata = $this->getClassMetadata($associationResourceClass);
84+
$associationFieldIdentifier = $associationMetadata->getIdentifierFieldNames()[0];
85+
$doctrineTypeField = $this->getDoctrineFieldType($associationFieldIdentifier, $associationResourceClass);
86+
87+
$associationAlias = $alias;
88+
$associationField = $field;
89+
90+
if ($metadata->isCollectionValuedAssociation($associationField) || $metadata->isAssociationInverseSide($field)) {
91+
$associationAlias = QueryBuilderHelper::addJoinOnce($queryBuilder, $queryNameGenerator, $alias, $associationField);
92+
$associationField = $associationFieldIdentifier;
93+
}
94+
95+
$values = $this->convertValuesToTheDatabaseRepresentationIfNecessary($queryBuilder, $doctrineTypeField, $values);
96+
$this->addWhere($queryBuilder, $queryNameGenerator, $associationAlias, $associationField, $values);
97+
}
98+
99+
/**
100+
* Converts values to their database representation.
101+
*/
102+
private function convertValuesToTheDatabaseRepresentationIfNecessary(QueryBuilder $queryBuilder, ?string $doctrineFieldType, array $values): array
103+
{
104+
if ($doctrineFieldType && Type::hasType($doctrineFieldType)) {
105+
$doctrineType = Type::getType($doctrineFieldType);
106+
$platform = $queryBuilder->getEntityManager()->getConnection()->getDatabasePlatform();
107+
$databaseValues = [];
108+
109+
foreach ($values as $value) {
110+
try {
111+
$databaseValues[] = $doctrineType->convertToDatabaseValue($value, $platform);
112+
} catch (ConversionException $e) {
113+
$this->logger->notice('Invalid value conversion value to its database representation', [
114+
'exception' => $e,
115+
]);
116+
$databaseValues[] = null;
117+
}
118+
}
119+
120+
return $databaseValues;
121+
}
122+
123+
return $values;
124+
}
125+
126+
/**
127+
* Adds where clause.
128+
*/
129+
private function addWhere(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $alias, string $field, mixed $values): void
130+
{
131+
if (!\is_array($values)) {
132+
$values = [$values];
133+
}
134+
135+
$valueParameter = ':'.$queryNameGenerator->generateParameterName($field);
136+
$aliasedField = \sprintf('%s.%s', $alias, $field);
137+
138+
if (1 === \count($values)) {
139+
$queryBuilder
140+
->andWhere($queryBuilder->expr()->eq($aliasedField, $valueParameter))
141+
->setParameter($valueParameter, $values[0], $this->getDoctrineParameterType());
142+
143+
return;
144+
}
145+
146+
$queryBuilder
147+
->andWhere($queryBuilder->expr()->in($aliasedField, $valueParameter))
148+
->setParameter($valueParameter, $values, $this->getDoctrineArrayParameterType());
149+
}
150+
151+
protected function getDoctrineParameterType(): ?ParameterType
152+
{
153+
return null;
154+
}
155+
156+
protected function getDoctrineArrayParameterType(): ?ArrayParameterType
157+
{
158+
return null;
159+
}
160+
161+
public function getOpenApiParameters(Parameter $parameter): array
162+
{
163+
$in = $parameter instanceof QueryParameter ? 'query' : 'header';
164+
$key = $parameter->getKey();
165+
166+
return [
167+
new OpenApiParameter(
168+
name: $key,
169+
in: $in,
170+
schema: self::UUID_SCHEMA,
171+
style: 'form',
172+
explode: false
173+
),
174+
new OpenApiParameter(
175+
name: $key.'[]',
176+
in: $in,
177+
description: 'One or more Uuids',
178+
schema: [
179+
'type' => 'array',
180+
'items' => self::UUID_SCHEMA,
181+
],
182+
style: 'deepObject',
183+
explode: true
184+
),
185+
];
186+
}
187+
188+
/**
189+
* Normalize the values array.
190+
*/
191+
protected function normalizeValues(array $values, string $property): ?array
192+
{
193+
foreach ($values as $key => $value) {
194+
if (!\is_string($value)) {
195+
unset($values[$key]);
196+
}
197+
}
198+
199+
if (0 === \count($values)) {
200+
$this->getLogger()->notice('Invalid filter ignored', [
201+
'exception' => new InvalidArgumentException(\sprintf('At least one value is required, multiple values should be in "%1$s[]=019b3c90-e265-72e5-a594-17b446a4067f&%1$s[]=019b3c9b-bce6-76dc-a066-9a44f4ec253f" format', $property)),
202+
]);
203+
204+
return null;
205+
}
206+
207+
return array_values($values);
208+
}
209+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
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\Doctrine\Orm\Filter;
15+
16+
use ApiPlatform\Metadata\Parameter;
17+
use ApiPlatform\Metadata\QueryParameter;
18+
use ApiPlatform\OpenApi\Model\Parameter as OpenApiParameter;
19+
20+
final class UlidFilter extends AbstractUuidFilter
21+
{
22+
private const ULID_SCHEMA = [
23+
'type' => 'string',
24+
'format' => 'ulid',
25+
];
26+
27+
public function getOpenApiParameters(Parameter $parameter): array
28+
{
29+
$in = $parameter instanceof QueryParameter ? 'query' : 'header';
30+
$key = $parameter->getKey();
31+
32+
return [
33+
new OpenApiParameter(
34+
name: $key,
35+
in: $in,
36+
schema: self::ULID_SCHEMA,
37+
style: 'form',
38+
explode: false
39+
),
40+
new OpenApiParameter(
41+
name: $key.'[]',
42+
in: $in,
43+
description: 'One or more Ulids',
44+
schema: [
45+
'type' => 'array',
46+
'items' => self::ULID_SCHEMA,
47+
],
48+
style: 'deepObject',
49+
explode: true
50+
),
51+
];
52+
}
53+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
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\Doctrine\Orm\Filter;
15+
16+
use Doctrine\DBAL\ArrayParameterType;
17+
use Doctrine\DBAL\ParameterType;
18+
19+
final class UuidBinaryFilter extends AbstractUuidFilter
20+
{
21+
protected function getDoctrineParameterType(): ParameterType
22+
{
23+
return ParameterType::BINARY;
24+
}
25+
26+
protected function getDoctrineArrayParameterType(): ArrayParameterType
27+
{
28+
return ArrayParameterType::BINARY;
29+
}
30+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
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\Doctrine\Orm\Filter;
15+
16+
final class UuidFilter extends AbstractUuidFilter
17+
{
18+
}

src/Symfony/Bundle/Resources/config/doctrine_orm.php

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,20 @@
222222
->parent('api_platform.doctrine.orm.search_filter')
223223
->args([[]]);
224224

225+
$services->set('api_platform.doctrine.orm.uuid_filter', 'ApiPlatform\Doctrine\Orm\Filter\UuidFilter')
226+
->arg(0, service('doctrine'))
227+
->arg(3, service('logger')->ignoreOnInvalid())
228+
->arg('$nameConverter', service('api_platform.name_converter')->ignoreOnInvalid());
229+
$services->alias('ApiPlatform\Doctrine\Orm\Filter\UuidFilter', 'api_platform.doctrine.orm.uuid_filter');
230+
231+
$services->set('api_platform.doctrine.orm.ulid_filter', 'ApiPlatform\Doctrine\Orm\Filter\UlidFilter')
232+
->parent('api_platform.doctrine.orm.uuid_filter');
233+
$services->alias('ApiPlatform\Doctrine\Orm\Filter\UlidFilter', 'api_platform.doctrine.orm.ulid_filter');
234+
235+
$services->set('api_platform.doctrine.orm.uuid_binary_filter', 'ApiPlatform\Doctrine\Orm\Filter\UuidBinaryFilter')
236+
->parent('api_platform.doctrine.orm.uuid_filter');
237+
$services->alias('ApiPlatform\Doctrine\Orm\Filter\UuidBinaryFilter', 'api_platform.doctrine.orm.uuid_binary_filter');
238+
225239
$services->set('api_platform.doctrine.orm.metadata.resource.metadata_collection_factory', 'ApiPlatform\Doctrine\Orm\Metadata\Resource\DoctrineOrmResourceCollectionMetadataFactory')
226240
->decorate('api_platform.metadata.resource.metadata_collection_factory', null, 40)
227241
->args([
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
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\Uuid;
15+
16+
use ApiPlatform\Doctrine\Orm\Filter\UuidBinaryFilter;
17+
use ApiPlatform\Metadata\ApiResource;
18+
use ApiPlatform\Metadata\Get;
19+
use ApiPlatform\Metadata\GetCollection;
20+
use ApiPlatform\Metadata\Post;
21+
use ApiPlatform\Metadata\QueryParameter;
22+
use Doctrine\ORM\Mapping as ORM;
23+
use Ramsey\Uuid\Uuid;
24+
use Ramsey\Uuid\UuidInterface;
25+
26+
#[ApiResource(operations: [
27+
new Get(),
28+
new GetCollection(
29+
parameters: [
30+
'id' => new QueryParameter(
31+
filter: new UuidBinaryFilter(),
32+
),
33+
]
34+
),
35+
new Post(),
36+
])]
37+
#[ORM\Entity]
38+
class RamseyUuidBinaryDevice
39+
{
40+
#[ORM\Id]
41+
#[ORM\Column(type: 'uuid_binary', unique: true)]
42+
public UuidInterface $id;
43+
44+
public function __construct(?UuidInterface $id = null)
45+
{
46+
$this->id = $id ?? Uuid::uuid7();
47+
}
48+
}

0 commit comments

Comments
 (0)