Completed
Pull Request — master (#812)
by Antoine
05:13
created

EagerLoadingExtension   A

Complexity

Total Complexity 25

Size/Duplication

Total Lines 147
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 8

Importance

Changes 0
Metric Value
dl 0
loc 147
c 0
b 0
f 0
wmc 25
lcom 1
cbo 8
rs 10

5 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 6 1
A getSerializerGroups() 0 18 4
A applyToCollection() 0 12 2
B applyToItem() 0 18 5
C joinRelations() 0 56 13
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 ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface;
18
use Doctrine\ORM\Mapping\ClassMetadataInfo;
19
use Doctrine\ORM\QueryBuilder;
20
21
/**
22
 * Eager loads relations.
23
 *
24
 * @author Charles Sarrazin <[email protected]>
25
 * @author Kévin Dunglas <[email protected]>
26
 * @author Antoine Bluchet <[email protected]>
27
 */
28
final class EagerLoadingExtension implements QueryCollectionExtensionInterface, QueryItemExtensionInterface
29
{
30
    const MAX_DEPTH = 100;
31
    private $propertyNameCollectionFactory;
32
    private $propertyMetadataFactory;
33
    private $resourceMetadataFactory;
34
35
    public function __construct(PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, ResourceMetadataFactoryInterface $resourceMetadataFactory)
36
    {
37
        $this->propertyMetadataFactory = $propertyMetadataFactory;
38
        $this->propertyNameCollectionFactory = $propertyNameCollectionFactory;
39
        $this->resourceMetadataFactory = $resourceMetadataFactory;
40
    }
41
42
    /**
43
     * Gets serializer groups once if available, if not it returns the $options array.
44
     *
45
     * @param array  $options       represents the operation name so that groups are the one of the specific operation
46
     * @param string $resourceClass
47
     * @param string $context       normalization_context or denormalization_context
48
     *
49
     * @return string[]
50
     */
51
    private function getSerializerGroups(string $resourceClass, array $options, string $context): array
52
    {
53
        $resourceMetadata = $this->resourceMetadataFactory->create($resourceClass);
54
55
        if (isset($options['collection_operation_name'])) {
56
            $context = $resourceMetadata->getCollectionOperationAttribute($options['collection_operation_name'], $context, null, true);
57
        } elseif (isset($options['item_operation_name'])) {
58
            $context = $resourceMetadata->getItemOperationAttribute($options['item_operation_name'], $context, null, true);
59
        } else {
60
            $context = $resourceMetadata->getAttribute($context);
61
        }
62
63
        if (empty($context['groups'])) {
64
            return $options;
65
        } else {
66
            return ['serializer_groups' => $context['groups']];
67
        }
68
    }
69
70
    /**
71
     * {@inheritdoc}
72
     */
73
    public function applyToCollection(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, string $operationName = null)
74
    {
75
        $options = [];
76
77
        if (null !== $operationName) {
78
            $options = ['collection_operation_name' => $operationName];
79
        }
80
81
        $groups = $this->getSerializerGroups($resourceClass, $options, 'normalization_context');
82
83
        $this->joinRelations($queryBuilder, $resourceClass, $groups);
84
    }
85
86
    /**
87
     * {@inheritdoc}
88
     */
89
    public function applyToItem(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, array $identifiers, string $operationName = null, array $context = [])
90
    {
91
        $options = [];
92
93
        if (null !== $operationName) {
94
            $options = ['item_operation_name' => $operationName];
95
        }
96
97
        if (isset($context['groups'])) {
98
            $groups = ['serializer_groups' => $context['groups']];
99
        } elseif (isset($context['resource_class'])) {
100
            $groups = $this->getSerializerGroups($context['resource_class'], $options, isset($context['api_denormalize']) ? 'denormalization_context' : 'normalization_context');
101
        } else {
102
            $groups = $this->getSerializerGroups($resourceClass, $options, 'normalization_context');
103
        }
104
105
        $this->joinRelations($queryBuilder, $resourceClass, $groups);
106
    }
107
108
    /**
109
     * Left joins relations to eager load.
110
     *
111
     * @param QueryBuilder $queryBuilder
112
     * @param string       $resourceClass
113
     * @param array        $propertyMetadataOptions
114
     * @param string       $originAlias             the current entity alias (first o, then a1, a2 etc.)
115
     * @param string       $relationAlias           the previous relation alias to keep it unique
116
     * @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
117
     */
118
    private function joinRelations(QueryBuilder $queryBuilder, string $resourceClass, array $propertyMetadataOptions = [], string $originAlias = 'o', string &$relationAlias = 'a', bool $wasLeftJoin = false, $depth = 0)
119
    {
120
        if ($depth > self::MAX_DEPTH) {
121
            throw new \RuntimeException('Too many joined relations, might be an infinite loop! Check out serialization groups!');
122
        }
123
124
        $entityManager = $queryBuilder->getEntityManager();
125
        $classMetadata = $entityManager->getClassMetadata($resourceClass);
126
        $j = 0;
127
        $i = 0;
128
129
        foreach ($classMetadata->associationMappings as $association => $mapping) {
130
            $propertyMetadata = $this->propertyMetadataFactory->create($resourceClass, $association, $propertyMetadataOptions);
131
132
            if (ClassMetadataInfo::FETCH_EAGER !== $mapping['fetch'] || false === $propertyMetadata->isReadableLink() || false === $propertyMetadata->isReadable()) {
133
                continue;
134
            }
135
136
            if (false === $wasLeftJoin) {
137
                $joinColumns = $mapping['joinColumns'] ?? $mapping['joinTable']['joinColumns'] ?? null;
138
139
                if (null === $joinColumns) {
140
                    $method = 'leftJoin';
141
                } else {
142
                    $method = false === $joinColumns[0]['nullable'] ? 'innerJoin' : 'leftJoin';
143
                }
144
            } else {
145
                $method = 'leftJoin';
146
            }
147
148
            $associationAlias = $relationAlias.$i++;
149
            $queryBuilder->{$method}($originAlias.'.'.$association, $associationAlias);
150
            $select = [];
151
            $targetClassMetadata = $entityManager->getClassMetadata($mapping['targetEntity']);
152
153
            foreach ($this->propertyNameCollectionFactory->create($mapping['targetEntity']) as $property) {
154
                $propertyMetadata = $this->propertyMetadataFactory->create($mapping['targetEntity'], $property, $propertyMetadataOptions);
155
156
                if (true === $propertyMetadata->isIdentifier()) {
157
                    $select[] = $property;
158
                    continue;
159
                }
160
161
                //the field test allows to add methods to a Resource which do not reflect real database fields
162
                if (true === $targetClassMetadata->hasField($property) && true === $propertyMetadata->isReadable()) {
163
                    $select[] = $property;
164
                }
165
            }
166
167
            $queryBuilder->addSelect(sprintf('partial %s.{%s}', $associationAlias, implode(',', $select)));
168
169
            $relationAlias = $relationAlias.++$j;
170
171
            $this->joinRelations($queryBuilder, $mapping['targetEntity'], $propertyMetadataOptions, $associationAlias, $relationAlias, $method === 'leftJoin', ++$depth);
172
        }
173
    }
174
}
175