From 0a483bd0e926dfdb796c37bebfa2b294f38f3a19 Mon Sep 17 00:00:00 2001 From: Ali Ben Macha Date: Sun, 1 Nov 2026 08:58:53 +0100 Subject: [PATCH 1/2] fix(doctrine): left join collection associations in IriFilter inside OR groups When an IriFilter on a collection association (e.g. `tags`) is grouped with other conditions in an OrFilter, it always added an INNER JOIN, so rows matching only the other branches but having an empty collection were dropped. It now uses the same join type helper as the nested property joins (#8674). --- src/Doctrine/Orm/Filter/IriFilter.php | 8 ++++- .../Filter/NestedPropertyJoinTypeTest.php | 29 +++++++++++++++++++ 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/src/Doctrine/Orm/Filter/IriFilter.php b/src/Doctrine/Orm/Filter/IriFilter.php index 5b8bde7ce5..c13064cea5 100644 --- a/src/Doctrine/Orm/Filter/IriFilter.php +++ b/src/Doctrine/Orm/Filter/IriFilter.php @@ -24,6 +24,7 @@ use ApiPlatform\State\ParameterProvider\IriConverterParameterProvider; use Doctrine\ORM\Mapping\ClassMetadata; use Doctrine\ORM\Mapping\FieldMapping; +use Doctrine\ORM\Query\Expr\Join; use Doctrine\ORM\QueryBuilder; /** @@ -68,7 +69,12 @@ public function apply(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $q // Collection associations (OneToMany/ManyToMany) require a JOIN to compare individual elements. if ($ormLeafMetadata['is_collection_valued']) { - $queryBuilder->join(\sprintf('%s.%s', $alias, $property), $parameterName); + // In an OR group, an INNER JOIN would drop the rows matched by the other branches + if (Join::LEFT_JOIN === $this->joinTypeFor($context)) { + $queryBuilder->leftJoin(\sprintf('%s.%s', $alias, $property), $parameterName); + } else { + $queryBuilder->innerJoin(\sprintf('%s.%s', $alias, $property), $parameterName); + } if (is_iterable($value)) { $queryBuilder diff --git a/src/Doctrine/Orm/Tests/Filter/NestedPropertyJoinTypeTest.php b/src/Doctrine/Orm/Tests/Filter/NestedPropertyJoinTypeTest.php index f3f91e5044..bc2fc79a15 100644 --- a/src/Doctrine/Orm/Tests/Filter/NestedPropertyJoinTypeTest.php +++ b/src/Doctrine/Orm/Tests/Filter/NestedPropertyJoinTypeTest.php @@ -13,8 +13,10 @@ namespace ApiPlatform\Doctrine\Orm\Tests\Filter; +use ApiPlatform\Doctrine\Orm\Filter\IriFilter; use ApiPlatform\Doctrine\Orm\Filter\PartialSearchFilter; use ApiPlatform\Doctrine\Orm\Tests\Fixtures\Entity\Dummy; +use ApiPlatform\Doctrine\Orm\Tests\Fixtures\Entity\RelatedDummy; use ApiPlatform\Doctrine\Orm\Util\QueryNameGenerator; use ApiPlatform\Metadata\QueryParameter; use Doctrine\ORM\EntityManagerInterface; @@ -50,6 +52,33 @@ public function testNestedJoinTypeDependsOnWhereClause(string $whereClause, stri $this->assertStringNotContainsString($unexpectedJoin, $dql); } + #[DataProvider('provideWhereClauses')] + public function testIriFilterCollectionJoinTypeDependsOnWhereClause(string $whereClause, string $expectedJoin, string $unexpectedJoin): void + { + $entityManager = $this->createStub(EntityManagerInterface::class); + $queryBuilder = (new QueryBuilder($entityManager))->select('o')->from(Dummy::class, 'o'); + $parameter = (new QueryParameter(key: 'relatedDummies', property: 'relatedDummies')) + ->withExtraProperties([ + '_api_values' => 'x', + 'nested_properties_info' => ['relatedDummies' => [ + 'relation_classes' => [], + 'converted_relation_segments' => [], + 'leaf_property' => 'relatedDummies', + 'orm_leaf_metadata' => [ + 'is_collection_valued' => true, + 'association_target_class' => RelatedDummy::class, + 'identifier_type' => null, + ], + ]], + ]); + + (new IriFilter())->apply($queryBuilder, new QueryNameGenerator(), Dummy::class, null, ['parameter' => $parameter, 'whereClause' => $whereClause]); + + $dql = $queryBuilder->getDQL(); + $this->assertStringContainsString($expectedJoin.' o.relatedDummies', $dql); + $this->assertStringNotContainsString($unexpectedJoin, $dql); + } + /** * @return iterable */ From b8c16852080b45eaa3bc7d92ed603429a97a6a09 Mon Sep 17 00:00:00 2001 From: soyuka Date: Tue, 3 Nov 2026 14:31:18 +0100 Subject: [PATCH 2/2] refactor(doctrine): drop IriFilter join comment --- src/Doctrine/Orm/Filter/IriFilter.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Doctrine/Orm/Filter/IriFilter.php b/src/Doctrine/Orm/Filter/IriFilter.php index c13064cea5..b27558ff2c 100644 --- a/src/Doctrine/Orm/Filter/IriFilter.php +++ b/src/Doctrine/Orm/Filter/IriFilter.php @@ -69,7 +69,6 @@ public function apply(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $q // Collection associations (OneToMany/ManyToMany) require a JOIN to compare individual elements. if ($ormLeafMetadata['is_collection_valued']) { - // In an OR group, an INNER JOIN would drop the rows matched by the other branches if (Join::LEFT_JOIN === $this->joinTypeFor($context)) { $queryBuilder->leftJoin(\sprintf('%s.%s', $alias, $property), $parameterName); } else {