Completed
Pull Request — 2.0 (#959)
by Kévin
02:58
created

EagerLoadingExtension::joinRelations()   C

Complexity

Conditions 13
Paths 12

Size

Total Lines 53
Code Lines 32

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 53
rs 6.3327
c 0
b 0
f 0
cc 13
eloc 32
nc 12
nop 8

How to fix   Long Method    Complexity    Many Parameters   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

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\QueryNameGeneratorInterface;
17
use ApiPlatform\Core\Exception\PropertyNotFoundException;
18
use ApiPlatform\Core\Exception\ResourceClassNotFoundException;
19
use ApiPlatform\Core\Exception\RuntimeException;
20
use ApiPlatform\Core\Metadata\Property\Factory\PropertyMetadataFactoryInterface;
21
use ApiPlatform\Core\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface;
22
use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface;
23
use ApiPlatform\Core\Serializer\SerializerContextBuilderInterface;
24
use Doctrine\ORM\Mapping\ClassMetadataInfo;
25
use Doctrine\ORM\QueryBuilder;
26
use Symfony\Component\HttpFoundation\RequestStack;
27
28
/**
29
 * Eager loads relations.
30
 *
31
 * @author Charles Sarrazin <[email protected]>
32
 * @author Kévin Dunglas <[email protected]>
33
 * @author Antoine Bluchet <[email protected]>
34
 * @author Baptiste Meyer <[email protected]>
35
 */
36
final class EagerLoadingExtension implements QueryCollectionExtensionInterface, QueryItemExtensionInterface
37
{
38
    private $propertyNameCollectionFactory;
39
    private $propertyMetadataFactory;
40
    private $resourceMetadataFactory;
41
    private $maxJoins;
42
    private $forceEager;
43
    private $serializerContextBuilder;
44
    private $requestStack;
45
46
    public function __construct(PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, ResourceMetadataFactoryInterface $resourceMetadataFactory, int $maxJoins = 30, bool $forceEager = true, RequestStack $requestStack = null, SerializerContextBuilderInterface $serializerContextBuilder = null)
47
    {
48
        $this->propertyNameCollectionFactory = $propertyNameCollectionFactory;
49
        $this->propertyMetadataFactory = $propertyMetadataFactory;
50
        $this->resourceMetadataFactory = $resourceMetadataFactory;
51
        $this->maxJoins = $maxJoins;
52
        $this->forceEager = $forceEager;
53
        $this->serializerContextBuilder = $serializerContextBuilder;
54
        $this->requestStack = $requestStack;
55
    }
56
57
    /**
58
     * {@inheritdoc}
59
     */
60
    public function applyToCollection(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, string $operationName = null)
61
    {
62
        $options = [];
63
64
        if (null !== $operationName) {
65
            $options = ['collection_operation_name' => $operationName];
66
        }
67
68
        $forceEager = $this->isForceEager($resourceClass, $options);
69
70
        $groups = $this->getSerializerGroups($resourceClass, $options, 'normalization_context');
71
72
        $this->joinRelations($queryBuilder, $queryNameGenerator, $resourceClass, $forceEager, $queryBuilder->getRootAliases()[0], $groups);
73
    }
74
75
    /**
76
     * {@inheritdoc}
77
     * The context may contain serialization groups which helps defining joined entities that are readable.
78
     */
79
    public function applyToItem(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, array $identifiers, string $operationName = null, array $context = [])
80
    {
81
        $options = [];
82
83
        if (null !== $operationName) {
84
            $options = ['item_operation_name' => $operationName];
85
        }
86
87
        $forceEager = $this->isForceEager($resourceClass, $options);
88
89
        if (isset($context['groups'])) {
90
            $groups = ['serializer_groups' => $context['groups']];
91
        } elseif (isset($context['resource_class'])) {
92
            $groups = $this->getSerializerGroups($context['resource_class'], $options, isset($context['api_denormalize']) ? 'denormalization_context' : 'normalization_context');
93
        } else {
94
            $groups = $this->getSerializerGroups($resourceClass, $options, 'normalization_context');
95
        }
96
97
        $this->joinRelations($queryBuilder, $queryNameGenerator, $resourceClass, $forceEager, $queryBuilder->getRootAliases()[0], $groups);
98
    }
99
100
    /**
101
     * Joins relations to eager load.
102
     *
103
     * @param QueryBuilder                $queryBuilder
104
     * @param QueryNameGeneratorInterface $queryNameGenerator
105
     * @param string                      $resourceClass
106
     * @param bool                        $forceEager
107
     * @param string                      $parentAlias
108
     * @param array                       $propertyMetadataOptions
109
     * @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
110
     * @param int                         $joinCount               the number of joins
111
     *
112
     * @throws RuntimeException when the max number of joins has been reached
113
     */
114
    private function joinRelations(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, bool $forceEager, string $parentAlias, array $propertyMetadataOptions = [], bool $wasLeftJoin = false, int &$joinCount = 0)
115
    {
116
        if ($joinCount > $this->maxJoins) {
117
            throw new RuntimeException('The total number of joined relations has exceeded the specified maximum. Raise the limit if necessary.');
118
        }
119
120
        $entityManager = $queryBuilder->getEntityManager();
121
        $classMetadata = $entityManager->getClassMetadata($resourceClass);
122
123
        foreach ($classMetadata->associationMappings as $association => $mapping) {
124
            try {
125
                $propertyMetadata = $this->propertyMetadataFactory->create($resourceClass, $association, $propertyMetadataOptions);
126
            } catch (PropertyNotFoundException $propertyNotFoundException) {
127
                //skip properties not found
128
                continue;
129
            } catch (ResourceClassNotFoundException $resourceClassNotFoundException) {
130
                //skip associations that are not resource classes
131
                continue;
132
            }
133
134
            if (false === $forceEager && ClassMetadataInfo::FETCH_EAGER !== $mapping['fetch']) {
135
                continue;
136
            }
137
138
            if (false === $propertyMetadata->isReadableLink() || false === $propertyMetadata->isReadable()) {
139
                continue;
140
            }
141
142
            $isNullable = $mapping['joinColumns'][0]['nullable'] ?? true;
143
            if (false !== $wasLeftJoin || true === $isNullable) {
144
                $method = 'leftJoin';
145
            } else {
146
                $method = 'innerJoin';
147
            }
148
149
            $associationAlias = $queryNameGenerator->generateJoinAlias($association);
150
            $queryBuilder->{$method}(sprintf('%s.%s', $parentAlias, $association), $associationAlias);
151
            ++$joinCount;
152
153
            try {
154
                $this->addSelect($queryBuilder, $mapping['targetEntity'], $associationAlias, $propertyMetadataOptions);
155
            } catch (ResourceClassNotFoundException $resourceClassNotFoundException) {
156
                continue;
157
            }
158
159
            if ($mapping['targetEntity'] === $resourceClass) {
160
                $queryBuilder->addSelect($associationAlias);
161
                continue;
162
            }
163
164
            $this->joinRelations($queryBuilder, $queryNameGenerator, $mapping['targetEntity'], $forceEager, $associationAlias, $propertyMetadataOptions, $method === 'leftJoin', $joinCount);
165
        }
166
    }
167
168
    private function addSelect(QueryBuilder $queryBuilder, string $entity, string $associationAlias, array $propertyMetadataOptions)
169
    {
170
        $select = [];
171
        $entityManager = $queryBuilder->getEntityManager();
172
        $targetClassMetadata = $entityManager->getClassMetadata($entity);
173
174
        foreach ($this->propertyNameCollectionFactory->create($entity) as $property) {
175
            $propertyMetadata = $this->propertyMetadataFactory->create($entity, $property, $propertyMetadataOptions);
176
177
            if (true === $propertyMetadata->isIdentifier()) {
178
                $select[] = $property;
179
                continue;
180
            }
181
182
            //the field test allows to add methods to a Resource which do not reflect real database fields
183
            if (true === $targetClassMetadata->hasField($property) && true === $propertyMetadata->isReadable()) {
184
                $select[] = $property;
185
            }
186
        }
187
188
        $queryBuilder->addSelect(sprintf('partial %s.{%s}', $associationAlias, implode(',', $select)));
189
    }
190
191
    /**
192
     * Gets serializer groups if available, if not it returns the $options array.
193
     *
194
     * @param string $resourceClass
195
     * @param array  $options       represents the operation name so that groups are the one of the specific operation
196
     * @param string $context       normalization_context or denormalization_context
197
     *
198
     * @return array
199
     */
200
    private function getSerializerGroups(string $resourceClass, array $options, string $context): array
201
    {
202
        $request = null;
203
204
        if (null !== $this->requestStack && null !== $this->serializerContextBuilder) {
205
            $request = $this->requestStack->getCurrentRequest();
206
        }
207
208
        if (null !== $this->serializerContextBuilder && null !== $request) {
209
            $contextFromRequest = $this->serializerContextBuilder->createFromRequest($request, $context === 'normalization_context');
210
211
            if (isset($contextFromRequest['groups'])) {
212
                return ['serializer_groups' => $contextFromRequest['groups']];
213
            }
214
        }
215
216
        $resourceMetadata = $this->resourceMetadataFactory->create($resourceClass);
217
218
        if (isset($options['collection_operation_name'])) {
219
            $context = $resourceMetadata->getCollectionOperationAttribute($options['collection_operation_name'], $context, null, true);
220
        } elseif (isset($options['item_operation_name'])) {
221
            $context = $resourceMetadata->getItemOperationAttribute($options['item_operation_name'], $context, null, true);
222
        } else {
223
            $context = $resourceMetadata->getAttribute($context);
224
        }
225
226
        if (empty($context['groups'])) {
227
            return $options;
228
        }
229
230
        return ['serializer_groups' => $context['groups']];
231
    }
232
233
    /**
234
     * Does an operation force eager?
235
     *
236
     * @param string $resourceClass
237
     * @param array  $options
238
     *
239
     * @return bool
240
     */
241
    private function isForceEager(string $resourceClass, array $options): bool
242
    {
243
        $resourceMetadata = $this->resourceMetadataFactory->create($resourceClass);
244
245
        if (isset($options['collection_operation_name'])) {
246
            $forceEager = $resourceMetadata->getCollectionOperationAttribute($options['collection_operation_name'], 'force_eager', null, true);
247
        } elseif (isset($options['item_operation_name'])) {
248
            $forceEager = $resourceMetadata->getItemOperationAttribute($options['item_operation_name'], 'force_eager', null, true);
249
        } else {
250
            $forceEager = $resourceMetadata->getAttribute('force_eager');
251
        }
252
253
        return is_bool($forceEager) ? $forceEager : $this->forceEager;
254
    }
255
}
256