Passed
Pull Request — 2.1 (#1675)
by
unknown
04:31 queued 01:47
created

EagerLoadingExtension   B

Complexity

Total Complexity 51

Size/Duplication

Total Lines 221
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
dl 0
loc 221
rs 8.3206
c 0
b 0
f 0
wmc 51

7 Methods

Rating   Name   Duplication   Size   Complexity  
A applyToItem() 0 11 3
C addSelect() 0 33 12
A getSerializerGroups() 0 7 2
A __construct() 0 11 1
A applyToCollection() 0 11 2
C joinRelations() 0 73 23
B getPropertyMetadataOptions() 0 21 8

How to fix   Complexity   

Complex Class

Complex classes like EagerLoadingExtension often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use EagerLoadingExtension, and based on these observations, apply Extract Interface, too.

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
declare(strict_types=1);
13
14
namespace ApiPlatform\Core\Bridge\Doctrine\Orm\Extension;
15
16
use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\EagerLoadingTrait;
17
use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryNameGeneratorInterface;
18
use ApiPlatform\Core\Exception\PropertyNotFoundException;
19
use ApiPlatform\Core\Exception\ResourceClassNotFoundException;
20
use ApiPlatform\Core\Exception\RuntimeException;
21
use ApiPlatform\Core\Metadata\Property\Factory\PropertyMetadataFactoryInterface;
22
use ApiPlatform\Core\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface;
23
use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface;
24
use ApiPlatform\Core\Serializer\SerializerContextBuilderInterface;
25
use Doctrine\ORM\Mapping\ClassMetadataInfo;
26
use Doctrine\ORM\QueryBuilder;
27
use Symfony\Component\HttpFoundation\RequestStack;
28
use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactoryInterface;
29
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
30
use Symfony\Component\Serializer\Normalizer\AbstractObjectNormalizer;
31
32
/**
33
 * Eager loads relations.
34
 *
35
 * @author Charles Sarrazin <[email protected]>
36
 * @author Kévin Dunglas <[email protected]>
37
 * @author Antoine Bluchet <[email protected]>
38
 * @author Baptiste Meyer <[email protected]>
39
 */
40
final class EagerLoadingExtension implements QueryCollectionExtensionInterface, QueryItemExtensionInterface
41
{
42
    use EagerLoadingTrait;
43
44
    private $propertyNameCollectionFactory;
45
    private $propertyMetadataFactory;
46
    private $classMetadataFactory;
47
    private $maxJoins;
48
    private $serializerContextBuilder;
49
    private $requestStack;
50
51
    /**
52
     * @TODO move $fetchPartial after $forceEager (@soyuka) in 3.0
53
     */
54
    public function __construct(PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, ResourceMetadataFactoryInterface $resourceMetadataFactory, int $maxJoins = 30, bool $forceEager = true, RequestStack $requestStack = null, SerializerContextBuilderInterface $serializerContextBuilder = null, bool $fetchPartial = false, ClassMetadataFactoryInterface $classMetadataFactory = null)
55
    {
56
        $this->propertyNameCollectionFactory = $propertyNameCollectionFactory;
57
        $this->propertyMetadataFactory = $propertyMetadataFactory;
58
        $this->resourceMetadataFactory = $resourceMetadataFactory;
59
        $this->classMetadataFactory = $classMetadataFactory;
60
        $this->maxJoins = $maxJoins;
61
        $this->forceEager = $forceEager;
62
        $this->fetchPartial = $fetchPartial;
63
        $this->serializerContextBuilder = $serializerContextBuilder;
64
        $this->requestStack = $requestStack;
65
    }
66
67
    /**
68
     * {@inheritdoc}
69
     */
70
    public function applyToCollection(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, string $operationName = null)
71
    {
72
        $options = null === $operationName ? [] : ['collection_operation_name' => $operationName];
73
74
        $forceEager = $this->shouldOperationForceEager($resourceClass, $options);
75
        $fetchPartial = $this->shouldOperationFetchPartial($resourceClass, $options);
76
        $serializerContext = $this->getPropertyMetadataOptions($resourceClass, 'normalization_context', $options);
77
78
        $groups = $this->getSerializerGroups($options, $serializerContext);
79
80
        $this->joinRelations($queryBuilder, $queryNameGenerator, $resourceClass, $forceEager, $fetchPartial, $queryBuilder->getRootAliases()[0], $groups, $serializerContext);
81
    }
82
83
    /**
84
     * The context may contain serialization groups which helps defining joined entities that are readable.
85
     */
86
    public function applyToItem(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, array $identifiers, string $operationName = null, array $context = [])
87
    {
88
        $options = null === $operationName ? [] : ['item_operation_name' => $operationName];
89
90
        $forceEager = $this->shouldOperationForceEager($resourceClass, $options);
91
        $fetchPartial = $this->shouldOperationFetchPartial($resourceClass, $options);
92
        $contextType = isset($context['api_denormalize']) ? 'denormalization_context' : 'normalization_context';
93
        $propertyMetadataOptions = $this->getPropertyMetadataOptions($context['resource_class'] ?? $resourceClass, $contextType, $options);
94
        $serializerGroups = $this->getSerializerGroups($options, $propertyMetadataOptions);
95
96
        $this->joinRelations($queryBuilder, $queryNameGenerator, $resourceClass, $forceEager, $fetchPartial, $queryBuilder->getRootAliases()[0], $serializerGroups, $propertyMetadataOptions);
97
    }
98
99
    /**
100
     * Joins relations to eager load.
101
     *
102
     * @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
103
     * @param int  $joinCount    the number of joins
104
     * @param int  $currentDepth the current max depth
105
     *
106
     * @throws RuntimeException when the max number of joins has been reached
107
     */
108
    private function joinRelations(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, bool $forceEager, bool $fetchPartial, string $parentAlias, array $propertyMetadataOptions = [], array $context = [], bool $wasLeftJoin = false, int &$joinCount = 0, int $currentDepth = null)
109
    {
110
        if ($joinCount > $this->maxJoins) {
111
            throw new RuntimeException('The total number of joined relations has exceeded the specified maximum. Raise the limit if necessary, or use the "max_depth" option of the Symfony serializer.');
112
        }
113
114
        $currentDepth = $currentDepth > 0 ? $currentDepth - 1 : $currentDepth;
115
        $entityManager = $queryBuilder->getEntityManager();
116
        $classMetadata = $entityManager->getClassMetadata($resourceClass);
117
        $attributesMetadata = $this->classMetadataFactory ? $this->classMetadataFactory->getMetadataFor($resourceClass)->getAttributesMetadata() : null;
118
119
        foreach ($classMetadata->associationMappings as $association => $mapping) {
120
            //Don't join if max depth is enabled and the current depth limit is reached
121
            if (isset($context[AbstractObjectNormalizer::ENABLE_MAX_DEPTH]) && 0 === $currentDepth) {
122
                continue;
123
            }
124
125
            try {
126
                $propertyMetadata = $this->propertyMetadataFactory->create($resourceClass, $association, $propertyMetadataOptions);
127
            } catch (PropertyNotFoundException $propertyNotFoundException) {
128
                //skip properties not found
129
                continue;
130
            } catch (ResourceClassNotFoundException $resourceClassNotFoundException) {
131
                //skip associations that are not resource classes
132
                continue;
133
            }
134
135
            // We don't want to interfere with doctrine on this association
136
            if (false === $forceEager && ClassMetadataInfo::FETCH_EAGER !== $mapping['fetch']) {
137
                continue;
138
            }
139
140
            if ((false === $propertyMetadata->isReadableLink() || false === $propertyMetadata->isReadable()) && false === $propertyMetadata->getAttribute('fetchEager', false)) {
141
                continue;
142
            }
143
144
            $isNullable = $mapping['joinColumns'][0]['nullable'] ?? true;
145
            if (false !== $wasLeftJoin || true === $isNullable) {
146
                $method = 'leftJoin';
147
            } else {
148
                $method = 'innerJoin';
149
            }
150
151
            $associationAlias = $queryNameGenerator->generateJoinAlias($association);
152
            $queryBuilder->{$method}(sprintf('%s.%s', $parentAlias, $association), $associationAlias);
153
            ++$joinCount;
154
155
            if (true === $fetchPartial) {
156
                try {
157
                    $this->addSelect($queryBuilder, $mapping['targetEntity'], $associationAlias, $propertyMetadataOptions);
158
                } catch (ResourceClassNotFoundException $resourceClassNotFoundException) {
159
                    continue;
160
                }
161
            } else {
162
                $queryBuilder->addSelect($associationAlias);
163
            }
164
165
            // Avoid recursion
166
            if ($mapping['targetEntity'] === $resourceClass) {
167
                $queryBuilder->addSelect($associationAlias);
168
                continue;
169
            }
170
171
            if (isset($attributesMetadata[$association])) {
172
                $maxDepth = $attributesMetadata[$association]->getMaxDepth();
173
174
                // The current depth is the lowest max depth available in the ancestor tree.
175
                if (null !== $maxDepth && (null === $currentDepth || $maxDepth < $currentDepth)) {
176
                    $currentDepth = $maxDepth;
177
                }
178
            }
179
180
            $this->joinRelations($queryBuilder, $queryNameGenerator, $mapping['targetEntity'], $forceEager, $fetchPartial, $associationAlias, $propertyMetadataOptions, $context, 'leftJoin' === $method, $joinCount, $currentDepth);
181
        }
182
    }
183
184
    private function addSelect(QueryBuilder $queryBuilder, string $entity, string $associationAlias, array $propertyMetadataOptions)
185
    {
186
        $select = [];
187
        $entityManager = $queryBuilder->getEntityManager();
188
        $targetClassMetadata = $entityManager->getClassMetadata($entity);
189
        if ($targetClassMetadata->subClasses) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $targetClassMetadata->subClasses of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
190
            $queryBuilder->addSelect($associationAlias);
191
        } else {
192
            foreach ($this->propertyNameCollectionFactory->create($entity) as $property) {
193
                $propertyMetadata = $this->propertyMetadataFactory->create($entity, $property, $propertyMetadataOptions);
194
195
                if (true === $propertyMetadata->isIdentifier()) {
196
                    $select[] = $property;
197
                    continue;
198
                }
199
200
                //the field test allows to add methods to a Resource which do not reflect real database fields
201
                if ($targetClassMetadata->hasField($property) && (true === $propertyMetadata->getAttribute('fetchable') || $propertyMetadata->isReadable())) {
202
                    $select[] = $property;
203
                }
204
205
                if (array_key_exists($property, $targetClassMetadata->embeddedClasses)) {
206
                    foreach ($this->propertyNameCollectionFactory->create($targetClassMetadata->embeddedClasses[$property]['class']) as $embeddedProperty) {
207
                        $propertyMetadata = $this->propertyMetadataFactory->create($entity, $property, $propertyMetadataOptions);
208
                        $propertyName = "$property.$embeddedProperty";
209
                        if ($targetClassMetadata->hasField($propertyName) && (true === $propertyMetadata->getAttribute('fetchable') || $propertyMetadata->isReadable())) {
210
                            $select[] = $propertyName;
211
                        }
212
                    }
213
                }
214
            }
215
216
            $queryBuilder->addSelect(sprintf('partial %s.{%s}', $associationAlias, implode(',', $select)));
217
        }
218
    }
219
220
    /**
221
     * Gets serializer context.
222
     *
223
     * @param string $contextType normalization_context or denormalization_context
224
     * @param array  $options     represents the operation name so that groups are the one of the specific operation
225
     */
226
    private function getPropertyMetadataOptions(string $resourceClass, string $contextType, array $options): array
227
    {
228
        $request = null;
229
        if (null !== $this->requestStack && null !== $this->serializerContextBuilder) {
230
            $request = $this->requestStack->getCurrentRequest();
231
        }
232
233
        if (null !== $this->serializerContextBuilder && null !== $request) {
234
            return $this->serializerContextBuilder->createFromRequest($request, 'normalization_context' === $contextType);
235
        }
236
237
        $resourceMetadata = $this->resourceMetadataFactory->create($resourceClass);
238
        if (isset($options['collection_operation_name'])) {
239
            $context = $resourceMetadata->getCollectionOperationAttribute($options['collection_operation_name'], $contextType, null, true);
240
        } elseif (isset($options['item_operation_name'])) {
241
            $context = $resourceMetadata->getItemOperationAttribute($options['item_operation_name'], $contextType, null, true);
242
        } else {
243
            $context = $resourceMetadata->getAttribute($contextType);
244
        }
245
246
        return $context ?: [];
247
    }
248
249
    /**
250
     * Gets serializer groups if available, if not it returns the $options array.
251
     *
252
     * @param array $options represents the operation name so that groups are the one of the specific operation
253
     */
254
    private function getSerializerGroups(array $options, array $context): array
255
    {
256
        if (!empty($context[AbstractNormalizer::GROUPS])) {
257
            $options['serializer_groups'] = $context[AbstractNormalizer::GROUPS];
258
        }
259
260
        return $options;
261
    }
262
}
263