Skip to content

Commit 8d972fd

Browse files
committed
have operator support multiple values for one-to-many relations #8708
many-to-many always worked and keep working, but one-to-many were broken SQL
1 parent a109676 commit 8d972fd

File tree

4 files changed

+55
-2
lines changed

4 files changed

+55
-2
lines changed

src/Definition/Operator/HaveOperatorType.php

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
namespace GraphQL\Doctrine\Definition\Operator;
66

77
use Doctrine\ORM\Mapping\ClassMetadata;
8+
use Doctrine\ORM\Mapping\ClassMetadataInfo;
89
use Doctrine\ORM\QueryBuilder;
910
use GraphQL\Doctrine\Factory\UniqueNameFactory;
1011
use GraphQL\Type\Definition\LeafType;
@@ -38,10 +39,29 @@ protected function getSingleValuedDqlCondition(UniqueNameFactory $uniqueNameFact
3839

3940
protected function getCollectionValuedDqlCondition(UniqueNameFactory $uniqueNameFactory, ClassMetadata $metadata, QueryBuilder $queryBuilder, string $alias, string $field, array $args): ?string
4041
{
42+
$association = $metadata->associationMappings[$field];
4143
$values = $uniqueNameFactory->createParameterName();
4244
$queryBuilder->setParameter($values, $args['values']);
4345
$not = $args['not'] ? 'NOT ' : '';
4446

47+
// For one-to-many we cannot rely on MEMBER OF, because it does not support multiple values (in SQL it always
48+
// use `=`, and not `IN()`). So we simulate an approximation of MEMBER OF that support multiple values. But it
49+
// does **not** support composite identifiers. And that is fine because it is an official limitation of this
50+
// library anyway.
51+
if ($association['type'] === ClassMetadataInfo::ONE_TO_MANY) {
52+
$id = $metadata->identifier[0];
53+
54+
$otherClassName = $association['targetEntity'];
55+
$otherAlias = $uniqueNameFactory->createAliasName($otherClassName);
56+
$otherField = $association['mappedBy'];
57+
$otherMetadata = $queryBuilder->getEntityManager()->getClassMetadata($otherClassName);
58+
$otherId = $otherMetadata->identifier[0];
59+
60+
$result = $not . "EXISTS (SELECT 1 FROM $otherClassName $otherAlias WHERE $otherAlias.$otherField = $alias.$id AND $otherAlias.$otherId IN (:$values))";
61+
62+
return $result;
63+
}
64+
4565
return ':' . $values . ' ' . $not . 'MEMBER OF ' . $alias . '.' . $field;
4666
}
4767
}

tests/Blog/Model/User.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,13 @@ final class User extends AbstractModel
5252
*/
5353
private $posts;
5454

55+
/**
56+
* @var \Doctrine\Common\Collections\Collection
57+
*
58+
* @ORM\ManyToMany(targetEntity="GraphQLTests\Doctrine\Blog\Model\Post")
59+
*/
60+
private $favoritePosts;
61+
5562
/**
5663
* @var null|User
5764
*

tests/Definition/Operator/OperatorsTest.php

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ public function providerOperator(): array
8989
'posts',
9090
],
9191
[
92-
':filter1 MEMBER OF alias.posts',
92+
'EXISTS (SELECT 1 FROM GraphQLTests\Doctrine\Blog\Model\Post post1 WHERE post1.user = alias.id AND post1.id IN (:filter1))',
9393
HaveOperatorType::class,
9494
[
9595
'values' => [123, 456],
@@ -98,7 +98,7 @@ public function providerOperator(): array
9898
'posts',
9999
],
100100
[
101-
':filter1 NOT MEMBER OF alias.posts',
101+
'NOT EXISTS (SELECT 1 FROM GraphQLTests\Doctrine\Blog\Model\Post post1 WHERE post1.user = alias.id AND post1.id IN (:filter1))',
102102
HaveOperatorType::class,
103103
[
104104
'values' => [123, 456],
@@ -130,6 +130,24 @@ public function providerOperator(): array
130130
],
131131
'manager',
132132
],
133+
[
134+
':filter1 MEMBER OF alias.favoritePosts',
135+
HaveOperatorType::class,
136+
[
137+
'values' => [123, 456],
138+
'not' => false,
139+
],
140+
'favoritePosts',
141+
],
142+
[
143+
':filter1 NOT MEMBER OF alias.favoritePosts',
144+
HaveOperatorType::class,
145+
[
146+
'values' => [123, 456],
147+
'not' => true,
148+
],
149+
'favoritePosts',
150+
],
133151
[
134152
null,
135153
EmptyOperatorType::class,

tests/data/PostFilter.graphqls

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -561,6 +561,7 @@ input UserFilterGroupCondition {
561561
isAdministrator: UserFilterGroupConditionIsAdministrator
562562
id: UserFilterGroupConditionId
563563
posts: UserFilterGroupConditionPosts
564+
favoritePosts: UserFilterGroupConditionFavoritePosts
564565
manager: UserFilterGroupConditionManager
565566
}
566567

@@ -592,6 +593,12 @@ input UserFilterGroupConditionEmail {
592593
group: GroupOperatorString
593594
}
594595

596+
"""Type to specify a condition on a specific field"""
597+
input UserFilterGroupConditionFavoritePosts {
598+
have: HaveOperatorID
599+
empty: EmptyOperatorID
600+
}
601+
595602
"""Type to specify a condition on a specific field"""
596603
input UserFilterGroupConditionId {
597604
like: LikeOperatorID
@@ -659,6 +666,7 @@ input UserFilterGroupConditionPosts {
659666
"""Type to specify join tables in a filter"""
660667
input UserFilterGroupJoin {
661668
posts: JoinOnPost
669+
favoritePosts: JoinOnPost
662670
manager: JoinOnUser
663671
}
664672

0 commit comments

Comments
 (0)