|
1
|
|
|
<?php |
|
2
|
|
|
|
|
3
|
|
|
/* |
|
4
|
|
|
* This file is part of the API Platform project. |
|
5
|
|
|
* |
|
6
|
|
|
* (c) Kévin Dunglas <[email protected]> |
|
7
|
|
|
* |
|
8
|
|
|
* For the full copyright and license information, please view the LICENSE |
|
9
|
|
|
* file that was distributed with this source code. |
|
10
|
|
|
*/ |
|
11
|
|
|
|
|
12
|
|
|
namespace ApiPlatform\Core\Bridge\Doctrine\Orm\Extension; |
|
13
|
|
|
|
|
14
|
|
|
use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryNameGeneratorInterface; |
|
15
|
|
|
use ApiPlatform\Core\Metadata\Property\Factory\PropertyMetadataFactoryInterface; |
|
16
|
|
|
use ApiPlatform\Core\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface; |
|
17
|
|
|
use Doctrine\ORM\Mapping\ClassMetadataInfo; |
|
18
|
|
|
use Doctrine\ORM\QueryBuilder; |
|
19
|
|
|
|
|
20
|
|
|
/** |
|
21
|
|
|
* Eager loads relations. |
|
22
|
|
|
* |
|
23
|
|
|
* @author Charles Sarrazin <[email protected]> |
|
24
|
|
|
* @author Kévin Dunglas <[email protected]> |
|
25
|
|
|
*/ |
|
26
|
|
|
final class EagerLoadingExtension implements QueryCollectionExtensionInterface, QueryItemExtensionInterface |
|
27
|
|
|
{ |
|
28
|
|
|
private $propertyNameCollectionFactory; |
|
29
|
|
|
private $propertyMetadataFactory; |
|
30
|
|
|
|
|
31
|
|
|
public function __construct(PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory) |
|
32
|
|
|
{ |
|
33
|
|
|
$this->propertyMetadataFactory = $propertyMetadataFactory; |
|
34
|
|
|
$this->propertyNameCollectionFactory = $propertyNameCollectionFactory; |
|
35
|
|
|
} |
|
36
|
|
|
|
|
37
|
|
|
/** |
|
38
|
|
|
* {@inheritdoc} |
|
39
|
|
|
*/ |
|
40
|
|
|
public function applyToCollection(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, string $operationName = null) |
|
41
|
|
|
{ |
|
42
|
|
|
$this->joinRelations($queryBuilder, $resourceClass); |
|
43
|
|
|
} |
|
44
|
|
|
|
|
45
|
|
|
/** |
|
46
|
|
|
* {@inheritdoc} |
|
47
|
|
|
*/ |
|
48
|
|
|
public function applyToItem(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, array $identifiers, string $operationName = null) |
|
49
|
|
|
{ |
|
50
|
|
|
$this->joinRelations($queryBuilder, $resourceClass); |
|
51
|
|
|
} |
|
52
|
|
|
|
|
53
|
|
|
public function getMetadataProperties($resourceClass): array |
|
54
|
|
|
{ |
|
55
|
|
|
$properties = []; |
|
56
|
|
|
|
|
57
|
|
|
foreach ($this->propertyNameCollectionFactory->create($resourceClass) as $property) { |
|
58
|
|
|
$properties[$property] = $this->propertyMetadataFactory->create($resourceClass, $property); |
|
59
|
|
|
} |
|
60
|
|
|
|
|
61
|
|
|
return $properties; |
|
62
|
|
|
} |
|
63
|
|
|
|
|
64
|
|
|
/** |
|
65
|
|
|
* Left joins relations to eager load. |
|
66
|
|
|
* |
|
67
|
|
|
* @param QueryBuilder $queryBuilder |
|
68
|
|
|
* @param string $resourceClass |
|
69
|
|
|
*/ |
|
70
|
|
|
private function joinRelations(QueryBuilder $queryBuilder, string $resourceClass, string $originAlias = 'o', string &$relationAlias = 'a') |
|
71
|
|
|
{ |
|
72
|
|
|
$classMetadata = $queryBuilder->getEntityManager()->getClassMetadata($resourceClass); |
|
73
|
|
|
$j = 0; |
|
74
|
|
|
|
|
75
|
|
|
foreach ($classMetadata->getAssociationNames() as $i => $association) { |
|
76
|
|
|
$mapping = $classMetadata->associationMappings[$association]; |
|
77
|
|
|
$propertyMetadata = $this->propertyMetadataFactory->create($resourceClass, $association); |
|
78
|
|
|
|
|
79
|
|
|
if (ClassMetadataInfo::FETCH_EAGER !== $mapping['fetch'] || false === $propertyMetadata->isReadableLink()) { |
|
80
|
|
|
continue; |
|
81
|
|
|
} |
|
82
|
|
|
|
|
83
|
|
|
$method = false === $mapping['joinColumns'][0]['nullable'] ? 'innerJoin' : 'leftJoin'; |
|
84
|
|
|
|
|
85
|
|
|
$associationAlias = $relationAlias.$i; |
|
86
|
|
|
$queryBuilder->{$method}($originAlias.'.'.$association, $associationAlias); |
|
87
|
|
|
$select = []; |
|
88
|
|
|
$targetClassMetadata = $queryBuilder->getEntityManager()->getClassMetadata($mapping['targetEntity']); |
|
89
|
|
|
|
|
90
|
|
|
//@TODO useless at the moment, but Doctrine queries non-necessary relations because addSelect($associationAlias) will try to fetch every |
|
91
|
|
|
//association belonging to the association, but if those are not serialized we don't need them ! Can be especially anoying when |
|
92
|
|
|
//the relation is a reverse relation (so doctrine does lots of subqueries). Here I need a way to tell doctrine to take only what we need. |
|
93
|
|
|
// |
|
94
|
|
|
//By using this array of properties, Doctrine will lazy fetch every associations, which are initially in an INNER JOIN (fetches them twice ?!) |
|
95
|
|
|
//If I try to use associationAlias when it has every property (so that Doctrine does not do sub queries on top of the initial one) it fails because it can not fetch parent association (like internally they try to find aliasX, but no aliasX is selected, we've instead aliasX.id, aliasX.name) |
|
96
|
|
|
//I obviously tried to specify `WITH` `aliasX.id = aliasY.id` without success |
|
97
|
|
|
foreach ($this->getMetadataProperties($mapping['targetEntity']) as $property => $propertyMetadata) { |
|
98
|
|
|
if (true === $targetClassMetadata->hasField($property) && true === $propertyMetadata->isReadableLink()) { |
|
99
|
|
|
$select[] = $associationAlias.'.'.$property; |
|
100
|
|
|
} |
|
101
|
|
|
} |
|
102
|
|
|
|
|
103
|
|
|
$queryBuilder->addSelect($associationAlias); |
|
104
|
|
|
|
|
105
|
|
|
$relationAlias = $relationAlias.++$j; |
|
106
|
|
|
$this->joinRelations($queryBuilder, $mapping['targetEntity'], $associationAlias, $relationAlias); |
|
107
|
|
|
} |
|
108
|
|
|
} |
|
109
|
|
|
} |
|
110
|
|
|
|