Skip to content

Commit a6b7796

Browse files
committed
fix(metadata): add uuid filter (#7465)
1 parent b728f87 commit a6b7796

22 files changed

+1395
-3
lines changed

composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@
3535
"conflict": {
3636
"doctrine/common": "<3.2.2",
3737
"doctrine/dbal": "<2.10",
38-
"doctrine/orm": "<2.14.0",
38+
"doctrine/orm": "<2.14.0 || 3.0.0",
3939
"doctrine/mongodb-odm": "<2.4",
4040
"doctrine/persistence": "<1.3",
4141
"symfony/framework-bundle": "6.4.6 || 7.0.6",
Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
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\Doctrine\Orm\Filter;
15+
16+
use ApiPlatform\Doctrine\Common\Filter\LoggerAwareInterface;
17+
use ApiPlatform\Doctrine\Common\Filter\LoggerAwareTrait;
18+
use ApiPlatform\Doctrine\Common\Filter\ManagerRegistryAwareInterface;
19+
use ApiPlatform\Doctrine\Common\Filter\ManagerRegistryAwareTrait;
20+
use ApiPlatform\Doctrine\Common\PropertyHelperTrait;
21+
use ApiPlatform\Doctrine\Orm\PropertyHelperTrait as OrmPropertyHelperTrait;
22+
use ApiPlatform\Doctrine\Orm\Util\QueryBuilderHelper;
23+
use ApiPlatform\Doctrine\Orm\Util\QueryNameGeneratorInterface;
24+
use ApiPlatform\Metadata\BackwardCompatibleFilterDescriptionTrait;
25+
use ApiPlatform\Metadata\Exception\InvalidArgumentException;
26+
use ApiPlatform\Metadata\JsonSchemaFilterInterface;
27+
use ApiPlatform\Metadata\OpenApiParameterFilterInterface;
28+
use ApiPlatform\Metadata\Operation;
29+
use ApiPlatform\Metadata\Parameter;
30+
use ApiPlatform\Metadata\QueryParameter;
31+
use ApiPlatform\OpenApi\Model\Parameter as OpenApiParameter;
32+
use Doctrine\DBAL\ArrayParameterType;
33+
use Doctrine\DBAL\ParameterType;
34+
use Doctrine\DBAL\Types\ConversionException;
35+
use Doctrine\DBAL\Types\Type;
36+
use Doctrine\ORM\Query\Expr\Join;
37+
use Doctrine\ORM\QueryBuilder;
38+
39+
/**
40+
* @internal
41+
*/
42+
class AbstractUuidFilter implements FilterInterface, ManagerRegistryAwareInterface, JsonSchemaFilterInterface, OpenApiParameterFilterInterface, LoggerAwareInterface
43+
{
44+
use BackwardCompatibleFilterDescriptionTrait;
45+
use LoggerAwareTrait;
46+
use ManagerRegistryAwareTrait;
47+
use OrmPropertyHelperTrait;
48+
use PropertyHelperTrait;
49+
50+
private const UUID_SCHEMA = [
51+
'type' => 'string',
52+
'format' => 'uuid',
53+
];
54+
55+
public function apply(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, ?Operation $operation = null, array $context = []): void
56+
{
57+
$parameter = $context['parameter'] ?? null;
58+
if (!$parameter) {
59+
return;
60+
}
61+
62+
$this->filterProperty($parameter->getProperty(), $parameter->getValue(), $queryBuilder, $queryNameGenerator, $resourceClass, $operation, $context);
63+
}
64+
65+
private function filterProperty(string $property, mixed $value, QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, ?Operation $operation = null, array $context = []): void
66+
{
67+
$alias = $queryBuilder->getRootAliases()[0];
68+
$field = $property;
69+
70+
$associations = [];
71+
if ($this->isPropertyNested($property, $resourceClass)) {
72+
[$alias, $field, $associations] = $this->addJoinsForNestedProperty($property, $alias, $queryBuilder, $queryNameGenerator, $resourceClass, Join::INNER_JOIN);
73+
}
74+
75+
$metadata = $this->getNestedMetadata($resourceClass, $associations);
76+
77+
if ($metadata->hasField($field)) {
78+
$value = $this->convertValuesToTheDatabaseRepresentationIfNecessary($queryBuilder, $this->getDoctrineFieldType($property, $resourceClass), $value);
79+
$this->addWhere($queryBuilder, $queryNameGenerator, $alias, $field, $value);
80+
81+
return;
82+
}
83+
84+
// metadata doesn't have the field, nor an association on the field
85+
if (!$metadata->hasAssociation($field)) {
86+
$this->logger->notice('Tried to filter on a non-existent field or association', [
87+
'field' => $field,
88+
'resource_class' => $resourceClass,
89+
'exception' => new InvalidArgumentException(\sprintf('Property "%s" does not exist in resource "%s".', $field, $resourceClass)),
90+
]);
91+
92+
return;
93+
}
94+
95+
// association, let's fetch the entity (or reference to it) if we can so we can make sure we get its orm id
96+
$associationResourceClass = $metadata->getAssociationTargetClass($field);
97+
$associationMetadata = $this->getClassMetadata($associationResourceClass);
98+
$associationFieldIdentifier = $associationMetadata->getIdentifierFieldNames()[0];
99+
$doctrineTypeField = $this->getDoctrineFieldType($associationFieldIdentifier, $associationResourceClass);
100+
101+
$associationAlias = $alias;
102+
$associationField = $field;
103+
104+
if ($metadata->isCollectionValuedAssociation($associationField) || $metadata->isAssociationInverseSide($field)) {
105+
$associationAlias = QueryBuilderHelper::addJoinOnce($queryBuilder, $queryNameGenerator, $alias, $associationField);
106+
$associationField = $associationFieldIdentifier;
107+
}
108+
109+
$value = $this->convertValuesToTheDatabaseRepresentationIfNecessary($queryBuilder, $doctrineTypeField, $value);
110+
$this->addWhere($queryBuilder, $queryNameGenerator, $associationAlias, $associationField, $value);
111+
}
112+
113+
/**
114+
* Converts values to their database representation.
115+
*/
116+
private function convertValuesToTheDatabaseRepresentationIfNecessary(QueryBuilder $queryBuilder, ?string $doctrineFieldType, mixed $value): mixed
117+
{
118+
if (null === $doctrineFieldType || !Type::hasType($doctrineFieldType)) {
119+
throw new InvalidArgumentException(\sprintf('The Doctrine type "%s" is not valid or not registered.', $doctrineFieldType));
120+
}
121+
122+
$doctrineType = Type::getType($doctrineFieldType);
123+
$platform = $queryBuilder->getEntityManager()->getConnection()->getDatabasePlatform();
124+
125+
if (\is_array($value)) {
126+
$databaseValues = [];
127+
foreach ($value as $val) {
128+
try {
129+
$databaseValues[] = $doctrineType->convertToDatabaseValue($val, $platform);
130+
} catch (ConversionException $e) {
131+
$this->logger->notice('Invalid value conversion to database representation', [
132+
'exception' => $e,
133+
]);
134+
}
135+
}
136+
137+
return $databaseValues;
138+
}
139+
140+
try {
141+
return $doctrineType->convertToDatabaseValue($value, $platform);
142+
} catch (ConversionException $e) {
143+
$this->logger->notice('Invalid value conversion to database representation', [
144+
'exception' => $e,
145+
]);
146+
147+
return null;
148+
}
149+
}
150+
151+
/**
152+
* Adds where clause.
153+
*/
154+
private function addWhere(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $alias, string $field, mixed $value): void
155+
{
156+
$valueParameter = ':'.$queryNameGenerator->generateParameterName($field);
157+
$aliasedField = \sprintf('%s.%s', $alias, $field);
158+
159+
if (!\is_array($value)) {
160+
$queryBuilder
161+
->andWhere($queryBuilder->expr()->eq($aliasedField, $valueParameter))
162+
->setParameter($valueParameter, $value, $this->getDoctrineParameterType());
163+
164+
return;
165+
}
166+
167+
$queryBuilder
168+
->andWhere($queryBuilder->expr()->in($aliasedField, $valueParameter))
169+
->setParameter($valueParameter, $value, $this->getDoctrineArrayParameterType());
170+
}
171+
172+
protected function getDoctrineParameterType(): ?ParameterType
173+
{
174+
return null;
175+
}
176+
177+
protected function getDoctrineArrayParameterType(): ?ArrayParameterType
178+
{
179+
return null;
180+
}
181+
182+
public function getOpenApiParameters(Parameter $parameter): array
183+
{
184+
$in = $parameter instanceof QueryParameter ? 'query' : 'header';
185+
$key = $parameter->getKey();
186+
187+
return [
188+
new OpenApiParameter(
189+
name: $key,
190+
in: $in,
191+
schema: self::UUID_SCHEMA,
192+
style: 'form',
193+
explode: false
194+
),
195+
new OpenApiParameter(
196+
name: $key.'[]',
197+
in: $in,
198+
description: 'One or more Uuids',
199+
schema: [
200+
'type' => 'array',
201+
'items' => self::UUID_SCHEMA,
202+
],
203+
style: 'deepObject',
204+
explode: true
205+
),
206+
];
207+
}
208+
209+
public function getSchema(Parameter $parameter): array
210+
{
211+
return self::UUID_SCHEMA;
212+
}
213+
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
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\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+
54+
public function getSchema(Parameter $parameter): array
55+
{
56+
return self::ULID_SCHEMA;
57+
}
58+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
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\Doctrine\Orm\Filter;
15+
16+
use Composer\InstalledVersions;
17+
use Composer\Semver\VersionParser;
18+
use Doctrine\DBAL\ArrayParameterType;
19+
use Doctrine\DBAL\ParameterType;
20+
21+
final class UuidBinaryFilter extends AbstractUuidFilter
22+
{
23+
public function __construct()
24+
{
25+
if (!InstalledVersions::satisfies(new VersionParser(), 'doctrine/orm', '^3.0.1')) {
26+
// @see https://github.com/doctrine/orm/pull/11287
27+
throw new \LogicException('The "doctrine/orm" package version 3.0.1 or higher is required to use the UuidBinaryFilter. Please upgrade your dependencies.');
28+
}
29+
}
30+
31+
protected function getDoctrineParameterType(): ParameterType
32+
{
33+
return ParameterType::BINARY;
34+
}
35+
36+
protected function getDoctrineArrayParameterType(): ArrayParameterType
37+
{
38+
return ArrayParameterType::BINARY;
39+
}
40+
}
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 <[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\Doctrine\Orm\Filter;
15+
16+
final class UuidFilter extends AbstractUuidFilter
17+
{
18+
}

src/Doctrine/Orm/composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
"api-platform/doctrine-common": "^4.2.9",
2828
"api-platform/metadata": "^4.2",
2929
"api-platform/state": "^4.2.4",
30-
"doctrine/orm": "^2.17 || ^3.0"
30+
"doctrine/orm": "^2.17 || ^3.0.1"
3131
},
3232
"require-dev": {
3333
"doctrine/doctrine-bundle": "^2.11 || ^3.1",

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,15 @@
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+
$services->alias('ApiPlatform\Doctrine\Orm\Filter\UuidFilter', 'api_platform.doctrine.orm.uuid_filter');
227+
228+
$services->set('api_platform.doctrine.orm.ulid_filter', 'ApiPlatform\Doctrine\Orm\Filter\UlidFilter');
229+
$services->alias('ApiPlatform\Doctrine\Orm\Filter\UlidFilter', 'api_platform.doctrine.orm.ulid_filter');
230+
231+
$services->set('api_platform.doctrine.orm.uuid_binary_filter', 'ApiPlatform\Doctrine\Orm\Filter\UuidBinaryFilter');
232+
$services->alias('ApiPlatform\Doctrine\Orm\Filter\UuidBinaryFilter', 'api_platform.doctrine.orm.uuid_binary_filter');
233+
225234
$services->set('api_platform.doctrine.orm.metadata.resource.metadata_collection_factory', 'ApiPlatform\Doctrine\Orm\Metadata\Resource\DoctrineOrmResourceCollectionMetadataFactory')
226235
->decorate('api_platform.metadata.resource.metadata_collection_factory', null, 40)
227236
->args([

0 commit comments

Comments
 (0)