Completed
Pull Request — master (#822)
by Antoine
03:15
created

EagerLoadingExtension::applyToCollection()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 16
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

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