Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 80 additions & 0 deletions features/doctrine/eager_loading.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
@!mongodb
Feature: Eager Loading
In order to have better performance
As a client software developer
The eager loading should be enabled

@createSchema
Scenario: Eager loading for a relation
Given there is a RelatedDummy with 2 friends
When I send a "GET" request to "/related_dummies/1"
And the response status code should be 200
And the DQL should be equal to:
"""
SELECT o, thirdLevel_a1, relatedToDummyFriend_a2, dummyFriend_a3
FROM ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\RelatedDummy o
LEFT JOIN o.thirdLevel thirdLevel_a1
LEFT JOIN o.relatedToDummyFriend relatedToDummyFriend_a2
LEFT JOIN relatedToDummyFriend_a2.dummyFriend dummyFriend_a3
WHERE o.id = :id_id
"""

Scenario: Eager loading for the search filter
Given there is a dummy object with a fourth level relation
When I send a "GET" request to "/dummies?relatedDummy.thirdLevel.level=3"
Then the response status code should be 200
And the DQL should be equal to:
"""
SELECT o
FROM ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\Dummy o
INNER JOIN o.relatedDummy relatedDummy_a1
INNER JOIN relatedDummy_a1.thirdLevel thirdLevel_a2
WHERE o IN(
SELECT o_a3
FROM ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\Dummy o_a3
INNER JOIN o_a3.relatedDummy relatedDummy_a4
INNER JOIN relatedDummy_a4.thirdLevel thirdLevel_a5
WHERE thirdLevel_a5.level = :level_p1
)
ORDER BY o.id ASC
"""

Scenario: Eager loading for a relation and a search filter
Given there is a RelatedDummy with 2 friends
When I send a "GET" request to "/related_dummies?relatedToDummyFriend.dummyFriend=2"
And the response status code should be 200
And the DQL should be equal to:
"""
SELECT o, thirdLevel_a4, relatedToDummyFriend_a1, dummyFriend_a5
FROM ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\RelatedDummy o
INNER JOIN o.relatedToDummyFriend relatedToDummyFriend_a1
LEFT JOIN o.thirdLevel thirdLevel_a4
INNER JOIN relatedToDummyFriend_a1.dummyFriend dummyFriend_a5
WHERE o IN(
SELECT o_a2
FROM ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\RelatedDummy o_a2
INNER JOIN o_a2.relatedToDummyFriend relatedToDummyFriend_a3
WHERE relatedToDummyFriend_a3.dummyFriend = :dummyFriend_p1
)
ORDER BY o.id ASC
"""

Scenario: Eager loading for a relation with complex sub-query filter
Given there is a RelatedDummy with 2 friends
When I send a "GET" request to "/related_dummies?complex_sub_query_filter=1"
Then the response status code should be 200
And the DQL should be equal to:
"""
SELECT o, thirdLevel_a3, relatedToDummyFriend_a4, dummyFriend_a5
FROM ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\RelatedDummy o
LEFT JOIN o.thirdLevel thirdLevel_a3
LEFT JOIN o.relatedToDummyFriend relatedToDummyFriend_a4
LEFT JOIN relatedToDummyFriend_a4.dummyFriend dummyFriend_a5
WHERE o.id IN (
SELECT related_dummy_a1.id
FROM ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\RelatedDummy related_dummy_a1
INNER JOIN related_dummy_a1.relatedToDummyFriend related_to_dummy_friend_a2
WITH related_to_dummy_friend_a2.name = :name_p1
)
ORDER BY o.id ASC
"""
24 changes: 15 additions & 9 deletions src/Bridge/Doctrine/Orm/Extension/EagerLoadingExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
namespace ApiPlatform\Core\Bridge\Doctrine\Orm\Extension;

use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\EagerLoadingTrait;
use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryBuilderHelper;
use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryNameGeneratorInterface;
use ApiPlatform\Core\Exception\InvalidArgumentException;
use ApiPlatform\Core\Exception\PropertyNotFoundException;
Expand All @@ -24,6 +25,7 @@
use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface;
use ApiPlatform\Core\Serializer\SerializerContextBuilderInterface;
use Doctrine\ORM\Mapping\ClassMetadataInfo;
use Doctrine\ORM\Query\Expr\Join;
use Doctrine\ORM\QueryBuilder;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactoryInterface;
Expand Down Expand Up @@ -190,16 +192,20 @@ private function joinRelations(QueryBuilder $queryBuilder, QueryNameGeneratorInt
continue;
}

$isNullable = $mapping['joinColumns'][0]['nullable'] ?? true;
if (false !== $wasLeftJoin || true === $isNullable) {
$method = 'leftJoin';
$existingJoin = QueryBuilderHelper::getExistingJoin($queryBuilder, $parentAlias, $association);

if (null !== $existingJoin) {
$associationAlias = $existingJoin->getAlias();
$isLeftJoin = Join::LEFT_JOIN === $existingJoin->getJoinType();
} else {
$method = 'innerJoin';
}
$isNullable = $mapping['joinColumns'][0]['nullable'] ?? true;
$isLeftJoin = false !== $wasLeftJoin || true === $isNullable;
$method = $isLeftJoin ? 'leftJoin' : 'innerJoin';

$associationAlias = $queryNameGenerator->generateJoinAlias($association);
$queryBuilder->{$method}(sprintf('%s.%s', $parentAlias, $association), $associationAlias);
++$joinCount;
$associationAlias = $queryNameGenerator->generateJoinAlias($association);
$queryBuilder->{$method}(sprintf('%s.%s', $parentAlias, $association), $associationAlias);
++$joinCount;
}

if (true === $fetchPartial) {
try {
Expand Down Expand Up @@ -230,7 +236,7 @@ private function joinRelations(QueryBuilder $queryBuilder, QueryNameGeneratorInt
}
}

$this->joinRelations($queryBuilder, $queryNameGenerator, $mapping['targetEntity'], $forceEager, $fetchPartial, $associationAlias, $options, $normalizationContext, 'leftJoin' === $method, $joinCount, $currentDepth);
$this->joinRelations($queryBuilder, $queryNameGenerator, $mapping['targetEntity'], $forceEager, $fetchPartial, $associationAlias, $options, $normalizationContext, $isLeftJoin, $joinCount, $currentDepth);
}
}

Expand Down
4 changes: 2 additions & 2 deletions src/Bridge/Doctrine/Orm/Util/EagerLoadingTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@

namespace ApiPlatform\Core\Bridge\Doctrine\Orm\Util;

use Doctrine\ORM\EntityManager;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Mapping\ClassMetadataInfo;

/**
Expand Down Expand Up @@ -66,7 +66,7 @@ private function getBooleanOperationAttribute(string $resourceClass, array $opti
*
* @param array $checked array cache of tested metadata classes
*/
private function hasFetchEagerAssociation(EntityManager $em, ClassMetadataInfo $classMetadata, array &$checked = []): bool
private function hasFetchEagerAssociation(EntityManagerInterface $em, ClassMetadataInfo $classMetadata, array &$checked = []): bool
{
$checked[] = $classMetadata->name;

Expand Down
2 changes: 1 addition & 1 deletion src/Bridge/Doctrine/Orm/Util/QueryBuilderHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ public static function traverseJoins(string $alias, QueryBuilder $queryBuilder,
/**
* Gets the existing join from QueryBuilder DQL parts.
*/
private static function getExistingJoin(QueryBuilder $queryBuilder, string $alias, string $association, string $originAlias = null): ?Join
public static function getExistingJoin(QueryBuilder $queryBuilder, string $alias, string $association, string $originAlias = null): ?Join
{
$parts = $queryBuilder->getDQLPart('join');
$rootAlias = $originAlias ?? $queryBuilder->getRootAliases()[0];
Expand Down
3 changes: 2 additions & 1 deletion src/Bridge/Symfony/Bundle/Resources/config/doctrine_orm.xml
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,8 @@
<argument type="service" id="serializer.mapping.class_metadata_factory" />

<tag name="api_platform.doctrine.orm.query_extension.item" priority="-8" />
<tag name="api_platform.doctrine.orm.query_extension.collection" priority="-8" />
<!-- After filter_eager_loading -->
<tag name="api_platform.doctrine.orm.query_extension.collection" priority="-18" />

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this priority change breaks filters that rely on existing joins from the eager loading extension because now the eager loading runs after the filters.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

exmaple filter that now breaks.

protected function filterProperty(string $property, $value, QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, string $operationName = null)
    {
        if ('searchTerm' !== $property || !in_array($resourceClass, [User::class, Employment::class]))
            return;

        $alias = $rootAlias = $queryBuilder->getRootAliases()[0];
        if (User::class !== $resourceClass) {
            $joins = $queryBuilder->getDQLPart("join");
            if (array_key_exists($rootAlias, $joins)) {
                /** @var \Doctrine\ORM\Query\Expr\Join $join */
                foreach ($joins[$rootAlias] as $join) {
                    if (sprintf('%s.user', $rootAlias) === $join->getJoin())
                        $alias = $join->getAlias();
                }
            }
        }

        $searchParam = $queryNameGenerator->generateParameterName('search');

        $conditions = [];
        $conditions[] = sprintf('%1$s.firstname LIKE :%2$s', $alias, $searchParam);
        $conditions[] = sprintf('%1$s.lastname LIKE :%2$s', $alias, $searchParam);
        $conditions[] = sprintf('CONCAT(%1$s.firstname, \' \', %1$s.lastname) LIKE :%2$s', $alias, $searchParam);
        $conditions[] = sprintf('%1$s.email LIKE :%2$s', $alias, $searchParam);

        $queryBuilder->andWhere(implode(' OR ', $conditions))
            ->setParameter($searchParam, $value . "%");
    }

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we could workaround this issue by adding the join explicitly in the filter, but now we join the relation two times, one by the filter and another by the eager loading extension.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we could workaround this issue by adding the join explicitly in the filter, but now we join the relation two times, one by the filter and another by the eager loading extension.

If the extension works correctly, the existing join is reused then.

@julienfalque do you think it's possible to solve the issue without changing the priority?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll have to check again, but as I remember this was required to prevent duplicating joins as described in #3487.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just had a look: changing this priority is required to get EagerLoadingExtension applied after FilterEagerLoadingExtension to fix #3487. I don't think FilterEagerLoadingExtension's priority can be changed (it has to be applied after all filters).

we could workaround this issue by adding the join explicitly in the filter, but now we join the relation two times, one by the filter and another by the eager loading extension.

If I remember correctly, the changes in this PR make EagerLoadingExtension able to rely on existing joins when possible instead of always adding its own. @zolex Are you sure you have duplicated joins if you add one in your filter?

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just assumed it because in earlier Versions it happened. I will check the resulting queries tomorrow and let you know.

</service>
<service id="ApiPlatform\Core\Bridge\Doctrine\Orm\Extension\EagerLoadingExtension" alias="api_platform.doctrine.orm.query_extension.eager_loading" />

Expand Down
20 changes: 20 additions & 0 deletions tests/Behat/DoctrineContext.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

namespace ApiPlatform\Core\Tests\Behat;

use ApiPlatform\Core\Tests\Fixtures\TestBundle\Doctrine\Orm\EntityManager;
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Document\AbsoluteUrlDummy as AbsoluteUrlDummyDocument;
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Document\AbsoluteUrlRelationDummy as AbsoluteUrlRelationDummyDocument;
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Document\Address as AddressDocument;
Expand Down Expand Up @@ -214,6 +215,25 @@ public function createDatabase()
$this->doctrine->getManager()->clear();
}

/**
* @Then the DQL should be equal to:
*/
public function theDqlShouldBeEqualTo(PyStringNode $dql)
{
/** @var EntityManager $manager */
$manager = $this->doctrine->getManager();

$actualDql = $manager::$dql;

$expectedDql = preg_replace('/\(\R */', '(', (string) $dql);
$expectedDql = preg_replace('/\R *\)/', ')', $expectedDql);
$expectedDql = preg_replace('/\R */', ' ', $expectedDql);

if ($expectedDql !== $actualDql) {
throw new \RuntimeException("The DQL:\n'$actualDql' is not equal to:\n'$expectedDql'");
}
}

/**
* @Given there are :nb dummy objects
*/
Expand Down
63 changes: 63 additions & 0 deletions tests/Bridge/Doctrine/Orm/Extension/EagerLoadingExtensionTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
use Doctrine\ORM\EntityManager;
use Doctrine\ORM\Mapping\ClassMetadata;
use Doctrine\ORM\Mapping\ClassMetadataInfo;
use Doctrine\ORM\Query\Expr\Join;
use Doctrine\ORM\QueryBuilder;
use PHPUnit\Framework\TestCase;
use Prophecy\Argument;
Expand Down Expand Up @@ -124,6 +125,7 @@ public function testApplyToCollection()
$queryBuilderProphecy->innerJoin('o.relatedDummy2', 'relatedDummy2_a2')->shouldBeCalledTimes(1);
$queryBuilderProphecy->addSelect('partial relatedDummy_a1.{id,name,embeddedDummy.name}')->shouldBeCalledTimes(1);
$queryBuilderProphecy->addSelect('partial relatedDummy2_a2.{id,name,embeddedDummy.name}')->shouldBeCalledTimes(1);
$queryBuilderProphecy->getDQLPart('join')->willReturn([]);

$queryBuilder = $queryBuilderProphecy->reveal();
$eagerExtensionTest = new EagerLoadingExtension($propertyNameCollectionFactoryProphecy->reveal(), $propertyMetadataFactoryProphecy->reveal(), $resourceMetadataFactoryProphecy->reveal(), 30, false, null, null, true);
Expand Down Expand Up @@ -230,6 +232,7 @@ public function testApplyToItem()
$queryBuilderProphecy->addSelect('partial relatedDummy3_a4.{id}')->shouldBeCalledTimes(1);
$queryBuilderProphecy->addSelect('partial relatedDummy4_a5.{id}')->shouldBeCalledTimes(1);
$queryBuilderProphecy->addSelect('singleInheritanceRelation_a6')->shouldBeCalledTimes(1);
$queryBuilderProphecy->getDQLPart('join')->willReturn([]);

$queryBuilder = $queryBuilderProphecy->reveal();
$orderExtensionTest = new EagerLoadingExtension($propertyNameCollectionFactoryProphecy->reveal(), $propertyMetadataFactoryProphecy->reveal(), $resourceMetadataFactoryProphecy->reveal(), 30, false, null, null, true);
Expand Down Expand Up @@ -377,6 +380,7 @@ public function testMaxJoinsReached()

$queryBuilderProphecy->innerJoin(Argument::type('string'), Argument::type('string'))->shouldBeCalled();
$queryBuilderProphecy->addSelect(Argument::type('string'))->shouldBeCalled();
$queryBuilderProphecy->getDQLPart('join')->willReturn([]);

$eagerExtensionTest = new EagerLoadingExtension($propertyNameCollectionFactoryProphecy->reveal(), $propertyMetadataFactoryProphecy->reveal(), $resourceMetadataFactoryProphecy->reveal(), 30, false, null, null, true);
$eagerExtensionTest->applyToCollection($queryBuilderProphecy->reveal(), new QueryNameGenerator(), Dummy::class, null, ['groups' => ['foo']]);
Expand Down Expand Up @@ -445,6 +449,7 @@ public function testMaxDepth()

$queryBuilderProphecy->innerJoin(Argument::type('string'), Argument::type('string'))->shouldBeCalledTimes(2);
$queryBuilderProphecy->addSelect(Argument::type('string'))->shouldBeCalled();
$queryBuilderProphecy->getDQLPart('join')->willReturn([]);

$eagerExtensionTest = new EagerLoadingExtension($propertyNameCollectionFactoryProphecy->reveal(), $propertyMetadataFactoryProphecy->reveal(), $resourceMetadataFactoryProphecy->reveal(), 30, false, null, null, true, $classMetadataFactoryProphecy->reveal());
$eagerExtensionTest->applyToCollection($queryBuilderProphecy->reveal(), new QueryNameGenerator(), Dummy::class);
Expand Down Expand Up @@ -486,6 +491,7 @@ public function testForceEager()

$queryBuilderProphecy->innerJoin('o.relation', 'relation_a1')->shouldBeCalledTimes(1);
$queryBuilderProphecy->addSelect('partial relation_a1.{id}')->shouldBeCalledTimes(1);
$queryBuilderProphecy->getDQLPart('join')->willReturn([]);

$queryBuilderProphecy->getRootAliases()->willReturn(['o']);
$queryBuilderProphecy->getEntityManager()->willReturn($emProphecy);
Expand Down Expand Up @@ -602,6 +608,7 @@ public function testResourceClassNotFoundExceptionPropertyNameCollection()
$queryBuilderProphecy->getRootAliases()->willReturn(['o']);
$queryBuilderProphecy->getEntityManager()->willReturn($emProphecy);
$queryBuilderProphecy->innerJoin('o.relation', 'relation_a1')->shouldBeCalledTimes(1);
$queryBuilderProphecy->getDQLPart('join')->willReturn([]);

$orderExtensionTest = new EagerLoadingExtension($propertyNameCollectionFactoryProphecy->reveal(), $propertyMetadataFactoryProphecy->reveal(), $resourceMetadataFactoryProphecy->reveal(), 30, true, null, null, true);
$orderExtensionTest->applyToItem($queryBuilderProphecy->reveal(), new QueryNameGenerator(), Dummy::class, []);
Expand Down Expand Up @@ -657,6 +664,7 @@ public function testApplyToCollectionWithSerializerContextBuilder()

$queryBuilderProphecy->leftJoin('o.relatedDummy', 'relatedDummy_a1')->shouldBeCalledTimes(1);
$queryBuilderProphecy->addSelect('partial relatedDummy_a1.{id,name}')->shouldBeCalledTimes(1);
$queryBuilderProphecy->getDQLPart('join')->willReturn([]);

$request = Request::create('/api/dummies', 'GET', []);

Expand Down Expand Up @@ -721,6 +729,7 @@ public function testAttributes()

$queryBuilderProphecy->leftJoin('o.relatedDummy', 'relatedDummy_a1')->shouldBeCalledTimes(1);
$queryBuilderProphecy->addSelect('partial relatedDummy_a1.{id,name}')->shouldBeCalledTimes(1);
$queryBuilderProphecy->getDQLPart('join')->willReturn([]);

$request = Request::create('/api/dummies', 'GET', []);

Expand Down Expand Up @@ -828,6 +837,7 @@ public function testOnlyOneRelationNotInAttributes()

$queryBuilderProphecy->leftJoin('o.relatedDummy', 'relatedDummy_a1')->shouldBeCalledTimes(1);
$queryBuilderProphecy->addSelect('partial relatedDummy_a1.{id,name}')->shouldBeCalledTimes(1);
$queryBuilderProphecy->getDQLPart('join')->willReturn([]);

$request = Request::create('/api/dummies', 'GET', []);

Expand Down Expand Up @@ -877,6 +887,7 @@ public function testApplyToCollectionNoPartial()
$queryBuilderProphecy->innerJoin('o.relatedDummy2', 'relatedDummy2_a2')->shouldBeCalledTimes(1);
$queryBuilderProphecy->addSelect('relatedDummy_a1')->shouldBeCalledTimes(1);
$queryBuilderProphecy->addSelect('relatedDummy2_a2')->shouldBeCalledTimes(1);
$queryBuilderProphecy->getDQLPart('join')->willReturn([]);

$queryBuilder = $queryBuilderProphecy->reveal();
$eagerExtensionTest = new EagerLoadingExtension($propertyNameCollectionFactoryProphecy->reveal(), $propertyMetadataFactoryProphecy->reveal(), $resourceMetadataFactoryProphecy->reveal(), 30);
Expand Down Expand Up @@ -933,12 +944,64 @@ private function doTestApplyToCollectionWithANonReadableButFetchEagerProperty(bo
$queryBuilderProphecy->innerJoin('o.relatedDummy2', 'relatedDummy2_a2')->shouldBeCalledTimes(1);
$queryBuilderProphecy->addSelect('relatedDummy_a1')->shouldBeCalledTimes(1);
$queryBuilderProphecy->addSelect('relatedDummy2_a2')->shouldBeCalledTimes(1);
$queryBuilderProphecy->getDQLPart('join')->willReturn([]);

$queryBuilder = $queryBuilderProphecy->reveal();
$eagerExtensionTest = new EagerLoadingExtension($propertyNameCollectionFactoryProphecy->reveal(), $propertyMetadataFactoryProphecy->reveal(), $resourceMetadataFactoryProphecy->reveal(), 30);
$eagerExtensionTest->applyToCollection($queryBuilder, new QueryNameGenerator(), Dummy::class);
}

/**
* @dataProvider provideExistingJoinCases
*/
public function testApplyToCollectionWithExistingJoin(string $joinType): void
{
$context = ['groups' => ['foo']];
$callContext = ['serializer_groups' => ['foo']];
$resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataFactoryInterface::class);
$resourceMetadataFactoryProphecy->create(Dummy::class)->willReturn(new ResourceMetadata());

$propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class);

$propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class);
$relationPropertyMetadata = new PropertyMetadata();
$relationPropertyMetadata = $relationPropertyMetadata->withReadableLink(true);

$propertyMetadataFactoryProphecy->create(Dummy::class, 'relatedDummy', $callContext)->willReturn($relationPropertyMetadata)->shouldBeCalled();

$queryBuilderProphecy = $this->prophesize(QueryBuilder::class);

$classMetadataProphecy = $this->prophesize(ClassMetadata::class);
$classMetadataProphecy->associationMappings = [
'relatedDummy' => ['fetch' => ClassMetadataInfo::FETCH_EAGER, 'joinColumns' => [['nullable' => true]], 'targetEntity' => RelatedDummy::class],
];

$relatedClassMetadataProphecy = $this->prophesize(ClassMetadata::class);

$emProphecy = $this->prophesize(EntityManager::class);
$emProphecy->getClassMetadata(Dummy::class)->shouldBeCalled()->willReturn($classMetadataProphecy->reveal());
$emProphecy->getClassMetadata(RelatedDummy::class)->shouldBeCalled()->willReturn($relatedClassMetadataProphecy->reveal());

$queryBuilderProphecy->getRootAliases()->willReturn(['o']);
$queryBuilderProphecy->getEntityManager()->willReturn($emProphecy);
$queryBuilderProphecy->getDQLPart('join')->willReturn([
'o' => [
new Join($joinType, 'o.relatedDummy', 'existing_join_alias'),
],
]);
$queryBuilderProphecy->addSelect('existing_join_alias')->shouldBeCalledTimes(1);

$queryBuilder = $queryBuilderProphecy->reveal();
$eagerExtensionTest = new EagerLoadingExtension($propertyNameCollectionFactoryProphecy->reveal(), $propertyMetadataFactoryProphecy->reveal(), $resourceMetadataFactoryProphecy->reveal(), 30, false);
$eagerExtensionTest->applyToCollection($queryBuilder, new QueryNameGenerator(), Dummy::class, null, $context);
}

public function provideExistingJoinCases(): iterable
{
yield [Join::LEFT_JOIN];
yield [Join::INNER_JOIN];
}

public function testApplyToCollectionWithAReadableButNotFetchEagerProperty()
{
$this->doTestApplyToCollectionWithAReadableButNotFetchEagerProperty(false);
Expand Down
Loading