diff --git a/src/Bridge/Doctrine/Orm/Extension/EagerLoadingExtension.php b/src/Bridge/Doctrine/Orm/Extension/EagerLoadingExtension.php index 9ee949bdbf5..a7af2075134 100644 --- a/src/Bridge/Doctrine/Orm/Extension/EagerLoadingExtension.php +++ b/src/Bridge/Doctrine/Orm/Extension/EagerLoadingExtension.php @@ -66,10 +66,11 @@ public function getMetadataProperties(string $resourceClass) : array * * @param QueryBuilder $queryBuilder * @param string $resourceClass - * @param string $originAlias - * @param string $relationAlias + * @param string $originAlias the current entity alias (first o, then a1, a2 etc.) + * @param string $relationAlias the previous relation alias to keep it unique + * @param bool $wasLeftJoin if the relation containing the new one had a left join, we have to force the new one to left join too */ - private function joinRelations(QueryBuilder $queryBuilder, string $resourceClass, string $originAlias = 'o', string &$relationAlias = 'a') + private function joinRelations(QueryBuilder $queryBuilder, string $resourceClass, string $originAlias = 'o', string &$relationAlias = 'a', bool $wasLeftJoin = false) { $classMetadata = $queryBuilder->getEntityManager()->getClassMetadata($resourceClass); $j = 0; @@ -82,12 +83,16 @@ private function joinRelations(QueryBuilder $queryBuilder, string $resourceClass continue; } - $joinColumns = $mapping['joinColumns'] ?? $mapping['joinTable']['joinColumns'] ?? null; + if (false === $wasLeftJoin) { + $joinColumns = $mapping['joinColumns'] ?? $mapping['joinTable']['joinColumns'] ?? null; - if (null === $joinColumns) { - $method = 'leftJoin'; + if (null === $joinColumns) { + $method = 'leftJoin'; + } else { + $method = false === $joinColumns[0]['nullable'] ? 'innerJoin' : 'leftJoin'; + } } else { - $method = false === $joinColumns[0]['nullable'] ? 'innerJoin' : 'leftJoin'; + $method = 'leftJoin'; } $associationAlias = $relationAlias.$i; @@ -110,7 +115,8 @@ private function joinRelations(QueryBuilder $queryBuilder, string $resourceClass $queryBuilder->addSelect(sprintf('partial %s.{%s}', $associationAlias, implode(',', $select))); $relationAlias = $relationAlias.++$j; - $this->joinRelations($queryBuilder, $mapping['targetEntity'], $associationAlias, $relationAlias); + + $this->joinRelations($queryBuilder, $mapping['targetEntity'], $associationAlias, $relationAlias, $method === 'leftJoin'); } } } diff --git a/tests/Bridge/Doctrine/Orm/Extension/EagerLoadingExtensionTest.php b/tests/Bridge/Doctrine/Orm/Extension/EagerLoadingExtensionTest.php index a8804e912b4..6ce7477db8b 100644 --- a/tests/Bridge/Doctrine/Orm/Extension/EagerLoadingExtensionTest.php +++ b/tests/Bridge/Doctrine/Orm/Extension/EagerLoadingExtensionTest.php @@ -19,6 +19,7 @@ use ApiPlatform\Core\Metadata\Property\PropertyNameCollection; use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\Dummy; use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\RelatedDummy; +use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\UnknownDummy; use Doctrine\ORM\EntityManager; use Doctrine\ORM\Mapping\ClassMetadata; use Doctrine\ORM\QueryBuilder; @@ -96,9 +97,10 @@ public function testApplyToItem() { $propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class); - $relatedNameCollection = new PropertyNameCollection(['id', 'name', 'notindatabase', 'notreadable']); + $relatedNameCollection = new PropertyNameCollection(['id', 'name', 'notindatabase', 'notreadable', 'relation']); $propertyNameCollectionFactoryProphecy->create(RelatedDummy::class)->willReturn($relatedNameCollection)->shouldBeCalled(); + $propertyNameCollectionFactoryProphecy->create(UnknownDummy::class)->willReturn(new PropertyNameCollection(['id']))->shouldBeCalled(); $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); $relationPropertyMetadata = new PropertyMetadata(); @@ -122,16 +124,19 @@ public function testApplyToItem() $propertyMetadataFactoryProphecy->create(RelatedDummy::class, 'name')->willReturn($namePropertyMetadata)->shouldBeCalled(); $propertyMetadataFactoryProphecy->create(RelatedDummy::class, 'notindatabase')->willReturn($notInDatabasePropertyMetadata)->shouldBeCalled(); $propertyMetadataFactoryProphecy->create(RelatedDummy::class, 'notreadable')->willReturn($notReadablePropertyMetadata)->shouldBeCalled(); + $propertyMetadataFactoryProphecy->create(RelatedDummy::class, 'relation')->willReturn($relationPropertyMetadata)->shouldBeCalled(); + $propertyMetadataFactoryProphecy->create(UnknownDummy::class, 'id')->willReturn($idPropertyMetadata)->shouldBeCalled(); $queryBuilderProphecy = $this->prophesize(QueryBuilder::class); $classMetadataProphecy = $this->prophesize(ClassMetadata::class); - $classMetadataProphecy->getAssociationNames()->shouldBeCalled()->willReturn([0 => 'relatedDummy', 'relatedDummy2', 'relatedDummy3', 'relatedDummy4']); + $classMetadataProphecy->getAssociationNames()->shouldBeCalled()->willReturn(['relatedDummy', 'relatedDummy2', 'relatedDummy3', 'relatedDummy4']); $classMetadataProphecy->associationMappings = [ 'relatedDummy' => ['fetch' => 3, 'joinColumns' => [['nullable' => true]], 'targetEntity' => RelatedDummy::class], - 'relatedDummy2' => ['fetch' => 3, 'joinColumns' => [['nullable' => false]], 'targetEntity' => RelatedDummy::class], - 'relatedDummy3' => ['fetch' => 3, 'joinTable' => ['joinColumns' => [['nullable' => false]]], 'targetEntity' => RelatedDummy::class], - 'relatedDummy4' => ['fetch' => 3, 'targetEntity' => RelatedDummy::class], + 'relatedDummy2' => ['fetch' => 3, 'joinColumns' => [['nullable' => false]], 'targetEntity' => UnknownDummy::class], + 'relatedDummy3' => ['fetch' => 3, 'joinTable' => ['joinColumns' => [['nullable' => false]]], 'targetEntity' => UnknownDummy::class], + 'relatedDummy4' => ['fetch' => 3, 'targetEntity' => UnknownDummy::class], + 'relatedDummy5' => ['fetch' => 2, 'targetEntity' => UnknownDummy::class], ]; $relatedClassMetadataProphecy = $this->prophesize(ClassMetadata::class); @@ -142,26 +147,35 @@ public function testApplyToItem() } } - $relatedClassMetadataProphecy->getAssociationNames()->shouldBeCalled()->willReturn([]); + $relatedClassMetadataProphecy->getAssociationNames()->shouldBeCalled()->willReturn(['relation']); + + $relatedClassMetadataProphecy->associationMappings = [ + 'relation' => ['fetch' => 3, 'joinColumns' => [['nullable' => false]], 'targetEntity' => UnknownDummy::class], + ]; + + $unknownClassMetadataProphecy = $this->prophesize(ClassMetadata::class); + $unknownClassMetadataProphecy->getAssociationNames()->shouldBeCalled()->willReturn([]); $emProphecy = $this->prophesize(EntityManager::class); $emProphecy->getClassMetadata(Dummy::class)->shouldBeCalled()->willReturn($classMetadataProphecy->reveal()); $emProphecy->getClassMetadata(RelatedDummy::class)->shouldBeCalled()->willReturn($relatedClassMetadataProphecy->reveal()); + $emProphecy->getClassMetadata(UnknownDummy::class)->shouldBeCalled()->willReturn($unknownClassMetadataProphecy->reveal()); $queryBuilderProphecy->leftJoin('o.relatedDummy', 'a0')->shouldBeCalled(1); - $queryBuilderProphecy->innerJoin('o.relatedDummy2', 'a11')->shouldBeCalled(1); - $queryBuilderProphecy->innerJoin('o.relatedDummy3', 'a122')->shouldBeCalled(1); - $queryBuilderProphecy->leftJoin('o.relatedDummy4', 'a1233')->shouldBeCalled(1); + $queryBuilderProphecy->leftJoin('a0.relation', 'a10')->shouldBeCalled(1); + $queryBuilderProphecy->innerJoin('o.relatedDummy2', 'a111')->shouldBeCalled(1); + $queryBuilderProphecy->innerJoin('o.relatedDummy3', 'a1122')->shouldBeCalled(1); + $queryBuilderProphecy->leftJoin('o.relatedDummy4', 'a11233')->shouldBeCalled(1); $queryBuilderProphecy->addSelect('partial a0.{id,name}')->shouldBeCalled(1); - $queryBuilderProphecy->addSelect('partial a11.{id,name}')->shouldBeCalled(1); - $queryBuilderProphecy->addSelect('partial a122.{id,name}')->shouldBeCalled(1); - $queryBuilderProphecy->addSelect('partial a1233.{id,name}')->shouldBeCalled(1); + $queryBuilderProphecy->addSelect('partial a10.{id}')->shouldBeCalled(1); + $queryBuilderProphecy->addSelect('partial a111.{id}')->shouldBeCalled(1); + $queryBuilderProphecy->addSelect('partial a1122.{id}')->shouldBeCalled(1); + $queryBuilderProphecy->addSelect('partial a11233.{id}')->shouldBeCalled(1); $queryBuilderProphecy->getEntityManager()->shouldBeCalled(2)->willReturn($emProphecy->reveal()); $queryBuilder = $queryBuilderProphecy->reveal(); $orderExtensionTest = new EagerLoadingExtension($propertyNameCollectionFactoryProphecy->reveal(), $propertyMetadataFactoryProphecy->reveal()); - $orderExtensionTest->applyToCollection($queryBuilder, new QueryNameGenerator(), Dummy::class); $orderExtensionTest->applyToItem($queryBuilder, new QueryNameGenerator(), Dummy::class, []); }