Skip to content

Commit 18c1a27

Browse files
soyukadunglas
authored andcommitted
Hotfix/filter eager loading fetch eager (#1028)
* Removed unneeded catch on ResourceClassNotFound * fix FilterEagerLoading when entity has FETCH_EAGER with no force eager
1 parent 8c0664a commit 18c1a27

File tree

3 files changed

+131
-32
lines changed

3 files changed

+131
-32
lines changed

src/Bridge/Doctrine/Orm/Extension/EagerLoadingExtension.php

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -61,13 +61,9 @@ public function applyToCollection(QueryBuilder $queryBuilder, QueryNameGenerator
6161

6262
$forceEager = $this->isForceEager($resourceClass, $options);
6363

64-
try {
65-
$groups = $this->getSerializerGroups($resourceClass, $options, 'normalization_context');
64+
$groups = $this->getSerializerGroups($resourceClass, $options, 'normalization_context');
6665

67-
$this->joinRelations($queryBuilder, $queryNameGenerator, $resourceClass, $forceEager, $queryBuilder->getRootAliases()[0], $groups);
68-
} catch (ResourceClassNotFoundException $resourceClassNotFoundException) {
69-
//ignore the not found exception
70-
}
66+
$this->joinRelations($queryBuilder, $queryNameGenerator, $resourceClass, $forceEager, $queryBuilder->getRootAliases()[0], $groups);
7167
}
7268

7369
/**

src/Bridge/Doctrine/Orm/Extension/FilterEagerLoadingExtension.php

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515

1616
use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryNameGeneratorInterface;
1717
use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface;
18+
use Doctrine\ORM\EntityManager;
19+
use Doctrine\ORM\Mapping\ClassMetadataInfo;
1820
use Doctrine\ORM\Query\Expr\Join;
1921
use Doctrine\ORM\QueryBuilder;
2022

@@ -38,7 +40,10 @@ public function __construct(ResourceMetadataFactoryInterface $resourceMetadataFa
3840
*/
3941
public function applyToCollection(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, string $operationName = null)
4042
{
41-
if (false === $this->forceEager || false === $this->isForceEager($resourceClass, ['collection_operation_name' => $operationName])) {
43+
$em = $queryBuilder->getEntityManager();
44+
$classMetadata = $em->getClassMetadata($resourceClass);
45+
46+
if (!$this->hasFetchEagerAssociation($em, $classMetadata) && (false === $this->forceEager || false === $this->isForceEager($resourceClass, ['collection_operation_name' => $operationName]))) {
4247
return;
4348
}
4449

@@ -59,8 +64,6 @@ public function applyToCollection(QueryBuilder $queryBuilder, QueryNameGenerator
5964
$queryBuilderClone = clone $queryBuilder;
6065
$queryBuilderClone->resetDQLPart('where');
6166

62-
$classMetadata = $queryBuilder->getEntityManager()->getClassMetadata($resourceClass);
63-
6467
if (!$classMetadata->isIdentifierComposite) {
6568
$replacementAlias = $queryNameGenerator->generateJoinAlias($originAlias);
6669
$in = $this->getQueryBuilderWithNewAliases($queryBuilder, $queryNameGenerator, $originAlias, $replacementAlias);
@@ -145,4 +148,27 @@ private function isForceEager(string $resourceClass, array $options): bool
145148

146149
return is_bool($forceEager) ? $forceEager : $this->forceEager;
147150
}
151+
152+
private function hasFetchEagerAssociation(EntityManager $em, ClassMetadataInfo $classMetadata, &$checked = [])
153+
{
154+
$checked[] = $classMetadata->name;
155+
156+
foreach ($classMetadata->associationMappings as $mapping) {
157+
if (ClassMetadataInfo::FETCH_EAGER === $mapping['fetch']) {
158+
return true;
159+
}
160+
161+
$related = $em->getClassMetadata($mapping['targetEntity']);
162+
163+
if (in_array($related->name, $checked)) {
164+
continue;
165+
}
166+
167+
if (true === $this->hasFetchEagerAssociation($em, $related, $checked)) {
168+
return true;
169+
}
170+
}
171+
172+
return false;
173+
}
148174
}

tests/Bridge/Doctrine/Orm/Extension/FilterEagerLoadingExtensionTest.php

Lines changed: 100 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717
use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryNameGeneratorInterface;
1818
use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface;
1919
use ApiPlatform\Core\Metadata\Resource\ResourceMetadata;
20+
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\CompositeItem;
21+
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\CompositeLabel;
2022
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\CompositeRelation;
2123
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\DummyCar;
2224
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\Foo;
@@ -39,8 +41,12 @@ public function testIsNoForceEagerCollectionAttributes()
3941
],
4042
], null));
4143

44+
$em = $this->prophesize(EntityManager::class);
45+
$em->getClassMetadata(DummyCar::class)->shouldBeCalled()->willReturn(new ClassMetadataInfo(DummyCar::class));
46+
4247
$qb = $this->prophesize(QueryBuilder::class);
4348
$qb->getDQLPart('where')->shouldNotBeCalled();
49+
$qb->getEntityManager()->willReturn($em);
4450

4551
$queryNameGenerator = $this->prophesize(QueryNameGeneratorInterface::class);
4652

@@ -50,13 +56,17 @@ public function testIsNoForceEagerCollectionAttributes()
5056

5157
public function testIsNoForceEagerResource()
5258
{
59+
$em = $this->prophesize(EntityManager::class);
60+
$em->getClassMetadata(DummyCar::class)->shouldBeCalled()->willReturn(new ClassMetadataInfo(DummyCar::class));
61+
5362
$resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataFactoryInterface::class);
5463
$resourceMetadataFactoryProphecy->create(DummyCar::class)->willReturn(new ResourceMetadata(DummyCar::class, null, null, null, [
5564
'get' => [],
5665
], ['force_eager' => false]));
5766

5867
$qb = $this->prophesize(QueryBuilder::class);
5968
$qb->getDQLPart('where')->shouldNotBeCalled();
69+
$qb->getEntityManager()->willReturn($em);
6070

6171
$queryNameGenerator = $this->prophesize(QueryNameGeneratorInterface::class);
6272

@@ -66,13 +76,17 @@ public function testIsNoForceEagerResource()
6676

6777
public function testIsForceEagerConfig()
6878
{
79+
$em = $this->prophesize(EntityManager::class);
80+
$em->getClassMetadata(DummyCar::class)->shouldBeCalled()->willReturn(new ClassMetadataInfo(DummyCar::class));
81+
6982
$resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataFactoryInterface::class);
7083
$resourceMetadataFactoryProphecy->create(DummyCar::class)->willReturn(new ResourceMetadata(DummyCar::class, null, null, null, [
7184
'get' => [],
7285
]));
7386

7487
$qb = $this->prophesize(QueryBuilder::class);
7588
$qb->getDQLPart('where')->shouldNotBeCalled();
89+
$qb->getEntityManager()->willReturn($em);
7690

7791
$queryNameGenerator = $this->prophesize(QueryNameGeneratorInterface::class);
7892

@@ -82,11 +96,15 @@ public function testIsForceEagerConfig()
8296

8397
public function testHasNoWherePart()
8498
{
99+
$em = $this->prophesize(EntityManager::class);
100+
$em->getClassMetadata(DummyCar::class)->shouldBeCalled()->willReturn(new ClassMetadataInfo(DummyCar::class));
101+
85102
$resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataFactoryInterface::class);
86103
$resourceMetadataFactoryProphecy->create(DummyCar::class)->willReturn(new ResourceMetadata(DummyCar::class));
87104

88105
$qb = $this->prophesize(QueryBuilder::class);
89106
$qb->getDQLPart('where')->shouldBeCalled()->willReturn(null);
107+
$qb->getEntityManager()->willReturn($em);
90108

91109
$queryNameGenerator = $this->prophesize(QueryNameGeneratorInterface::class);
92110

@@ -96,12 +114,16 @@ public function testHasNoWherePart()
96114

97115
public function testHasNoJoinPart()
98116
{
117+
$em = $this->prophesize(EntityManager::class);
118+
$em->getClassMetadata(DummyCar::class)->shouldBeCalled()->willReturn(new ClassMetadataInfo(DummyCar::class));
119+
99120
$resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataFactoryInterface::class);
100121
$resourceMetadataFactoryProphecy->create(DummyCar::class)->willReturn(new ResourceMetadata(DummyCar::class));
101122

102123
$qb = $this->prophesize(QueryBuilder::class);
103124
$qb->getDQLPart('where')->shouldBeCalled()->willReturn(new Expr\Andx());
104125
$qb->getDQLPart('join')->shouldBeCalled()->willReturn(null);
126+
$qb->getEntityManager()->willReturn($em);
105127

106128
$queryNameGenerator = $this->prophesize(QueryNameGeneratorInterface::class);
107129

@@ -164,12 +186,12 @@ public function testHiddenOrderBy()
164186
$filterEagerLoadingExtension->applyToCollection($qb, $queryNameGenerator->reveal(), DummyCar::class, 'get');
165187

166188
$expected = <<<SQL
167-
SELECT o, CASE WHEN o.dateCreated IS NULL THEN 0 ELSE 1 END AS HIDDEN _o_dateCreated_null_rank
168-
FROM ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\DummyCar o
169-
LEFT JOIN o.colors colors
189+
SELECT o, CASE WHEN o.dateCreated IS NULL THEN 0 ELSE 1 END AS HIDDEN _o_dateCreated_null_rank
190+
FROM ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\DummyCar o
191+
LEFT JOIN o.colors colors
170192
WHERE o IN(
171-
SELECT o_2 FROM ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\DummyCar o_2
172-
LEFT JOIN o_2.colors colors_2
193+
SELECT o_2 FROM ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\DummyCar o_2
194+
LEFT JOIN o_2.colors colors_2
173195
WHERE o_2.colors = :foo
174196
) ORDER BY _o_dateCreated_null_rank DESC ASC
175197
SQL;
@@ -204,15 +226,15 @@ public function testGroupBy()
204226
$filterEagerLoadingExtension->applyToCollection($qb, $queryNameGenerator->reveal(), DummyCar::class, 'get');
205227

206228
$expected = <<<SQL
207-
SELECT o, count(o.id) as counter
208-
FROM ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\DummyCar o
209-
LEFT JOIN o.colors colors WHERE o
229+
SELECT o, count(o.id) as counter
230+
FROM ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\DummyCar o
231+
LEFT JOIN o.colors colors WHERE o
210232
IN(
211-
SELECT o_2 FROM ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\DummyCar o_2
212-
LEFT JOIN o_2.colors colors_2
233+
SELECT o_2 FROM ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\DummyCar o_2
234+
LEFT JOIN o_2.colors colors_2
213235
WHERE o_2.colors = :foo
214-
)
215-
GROUP BY o.colors HAVING counter > 3
236+
)
237+
GROUP BY o.colors HAVING counter > 3
216238
ORDER BY o.colors ASC
217239
SQL;
218240

@@ -250,19 +272,74 @@ public function testCompositeIdentifiers()
250272
$filterEagerLoadingExtension->applyToCollection($qb, $queryNameGenerator->reveal(), CompositeRelation::class, 'get');
251273

252274
$expected = <<<SQL
253-
SELECT o
254-
FROM ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\CompositeRelation o
255-
INNER JOIN o.compositeItem item
256-
INNER JOIN o.compositeLabel label
275+
SELECT o
276+
FROM ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\CompositeRelation o
277+
INNER JOIN o.compositeItem item
278+
INNER JOIN o.compositeLabel label
279+
WHERE o.item IN(
280+
SELECT IDENTITY(o_2.item) FROM ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\CompositeRelation o_2
281+
INNER JOIN o_2.compositeItem item_2
282+
INNER JOIN o_2.compositeLabel label_2
283+
WHERE item_2.field1 = :foo
284+
) AND o.label IN(
285+
SELECT IDENTITY(o_2.label) FROM ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\CompositeRelation o_2
286+
INNER JOIN o_2.compositeItem item_2
287+
INNER JOIN o_2.compositeLabel label_2
288+
WHERE item_2.field1 = :foo
289+
)
290+
SQL;
291+
292+
$this->assertEquals($this->toDQLString($expected), $qb->getDQL());
293+
}
294+
295+
public function testFetchEagerWithNoForceEager()
296+
{
297+
$resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataFactoryInterface::class);
298+
$resourceMetadataFactoryProphecy->create(CompositeRelation::class)->willReturn(new ResourceMetadata(CompositeRelation::class));
299+
300+
$classMetadata = new ClassMetadataInfo(CompositeRelation::class);
301+
$classMetadata->isIdentifierComposite = true;
302+
$classMetadata->identifier = ['item', 'label'];
303+
$classMetadata->associationMappings = [
304+
'item' => ['fetch' => 3, 'joinColumns' => [['nullable' => false]], 'targetEntity' => CompositeItem::class],
305+
'label' => ['fetch' => 3, 'joinColumns' => [['nullable' => false]], 'targetEntity' => CompositeLabel::class],
306+
];
307+
308+
$em = $this->prophesize(EntityManager::class);
309+
$em->getExpressionBuilder()->shouldBeCalled()->willReturn(new Expr());
310+
$em->getClassMetadata(CompositeRelation::class)->shouldBeCalled()->willReturn($classMetadata);
311+
312+
$qb = new QueryBuilder($em->reveal());
313+
314+
$qb->select('o')
315+
->from(CompositeRelation::class, 'o')
316+
->innerJoin('o.compositeItem', 'item')
317+
->innerJoin('o.compositeLabel', 'label')
318+
->where('item.field1 = :foo')
319+
->setParameter('foo', 1);
320+
321+
$queryNameGenerator = $this->prophesize(QueryNameGeneratorInterface::class);
322+
$queryNameGenerator->generateJoinAlias('item')->shouldBeCalled()->willReturn('item_2');
323+
$queryNameGenerator->generateJoinAlias('label')->shouldBeCalled()->willReturn('label_2');
324+
$queryNameGenerator->generateJoinAlias('o')->shouldBeCalled()->willReturn('o_2');
325+
326+
$filterEagerLoadingExtension = new FilterEagerLoadingExtension($resourceMetadataFactoryProphecy->reveal(), false);
327+
$filterEagerLoadingExtension->applyToCollection($qb, $queryNameGenerator->reveal(), CompositeRelation::class, 'get');
328+
329+
$expected = <<<SQL
330+
SELECT o
331+
FROM ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\CompositeRelation o
332+
INNER JOIN o.compositeItem item
333+
INNER JOIN o.compositeLabel label
257334
WHERE o.item IN(
258-
SELECT IDENTITY(o_2.item) FROM ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\CompositeRelation o_2
259-
INNER JOIN o_2.compositeItem item_2
260-
INNER JOIN o_2.compositeLabel label_2
335+
SELECT IDENTITY(o_2.item) FROM ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\CompositeRelation o_2
336+
INNER JOIN o_2.compositeItem item_2
337+
INNER JOIN o_2.compositeLabel label_2
261338
WHERE item_2.field1 = :foo
262339
) AND o.label IN(
263-
SELECT IDENTITY(o_2.label) FROM ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\CompositeRelation o_2
264-
INNER JOIN o_2.compositeItem item_2
265-
INNER JOIN o_2.compositeLabel label_2
340+
SELECT IDENTITY(o_2.label) FROM ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\CompositeRelation o_2
341+
INNER JOIN o_2.compositeItem item_2
342+
INNER JOIN o_2.compositeLabel label_2
266343
WHERE item_2.field1 = :foo
267344
)
268345
SQL;
@@ -272,6 +349,6 @@ public function testCompositeIdentifiers()
272349

273350
private function toDQLString(string $dql): string
274351
{
275-
return preg_replace('/\\r\\n|\\n/', '', str_replace(' ', '', $dql));
352+
return preg_replace(['/\s+/', '/\(\s/', '/\s\)/'], [' ', '(', ')'], $dql);
276353
}
277354
}

0 commit comments

Comments
 (0)