Completed
Pull Request — master (#717)
by Antoine
06:28 queued 02:16
created

EagerLoadingExtension::getMetadataProperties()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 10
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 10
rs 9.4285
cc 2
eloc 5
nc 2
nop 1
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