Completed
Pull Request — 2.0 (#1100)
by Antoine
04:08
created

EagerLoadingExtension   B

Complexity

Total Complexity 38

Size/Duplication

Total Lines 209
Duplicated Lines 3.35 %

Coupling/Cohesion

Components 1
Dependencies 13

Importance

Changes 0
Metric Value
wmc 38
lcom 1
cbo 13
dl 7
loc 209
rs 8.3999
c 0
b 0
f 0

6 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 11 1
A applyToCollection() 0 15 2
B applyToItem() 0 21 5
C joinRelations() 0 59 14
C addSelect() 0 25 7
D getSerializerGroups() 7 32 9

How to fix   Duplicated Code   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

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

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
232
            $context = $resourceMetadata->getCollectionOperationAttribute($options['collection_operation_name'], $context, null, true);
233
        } elseif (isset($options['item_operation_name'])) {
234
            $context = $resourceMetadata->getItemOperationAttribute($options['item_operation_name'], $context, null, true);
235
        } else {
236
            $context = $resourceMetadata->getAttribute($context);
237
        }
238
239
        if (empty($context['groups'])) {
240
            return $options;
241
        }
242
243
        return ['serializer_groups' => $context['groups']];
244
    }
245
}
246