Completed
Push — master ( d130ed...16dd93 )
by Kévin
15s
created

EagerLoadingExtension   A

Complexity

Total Complexity 26

Size/Duplication

Total Lines 162
Duplicated Lines 4.94 %

Coupling/Cohesion

Components 1
Dependencies 8

Importance

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

6 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 8 8 1
A getSerializerGroups() 0 18 4
A applyToCollection() 0 13 2
B applyToItem() 0 20 5
D joinRelations() 0 37 10
A isForceEager() 0 14 4

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
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
 * @author Baptiste Meyer <[email protected]>
29
 */
30
final class EagerLoadingExtension implements QueryCollectionExtensionInterface, QueryItemExtensionInterface
31
{
32
    private $propertyNameCollectionFactory;
33
    private $propertyMetadataFactory;
34
    private $resourceMetadataFactory;
35
    private $maxJoins;
36
    private $forceEager;
37
38 View Code Duplication
    public function __construct(PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, ResourceMetadataFactoryInterface $resourceMetadataFactory, int $maxJoins = 30, bool $forceEager = true)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in 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...
39
    {
40
        $this->propertyMetadataFactory = $propertyMetadataFactory;
41
        $this->propertyNameCollectionFactory = $propertyNameCollectionFactory;
42
        $this->resourceMetadataFactory = $resourceMetadataFactory;
43
        $this->maxJoins = $maxJoins;
44
        $this->forceEager = $forceEager;
45
    }
46
47
    /**
48
     * Gets serializer groups once if available, if not it returns the $options array.
49
     *
50
     * @param array  $options       represents the operation name so that groups are the one of the specific operation
51
     * @param string $resourceClass
52
     * @param string $context       normalization_context or denormalization_context
53
     *
54
     * @return string[]
55
     */
56
    private function getSerializerGroups(string $resourceClass, array $options, string $context): array
57
    {
58
        $resourceMetadata = $this->resourceMetadataFactory->create($resourceClass);
59
60
        if (isset($options['collection_operation_name'])) {
61
            $context = $resourceMetadata->getCollectionOperationAttribute($options['collection_operation_name'], $context, null, true);
62
        } elseif (isset($options['item_operation_name'])) {
63
            $context = $resourceMetadata->getItemOperationAttribute($options['item_operation_name'], $context, null, true);
64
        } else {
65
            $context = $resourceMetadata->getAttribute($context);
66
        }
67
68
        if (empty($context['groups'])) {
69
            return $options;
70
        }
71
72
        return ['serializer_groups' => $context['groups']];
73
    }
74
75
    /**
76
     * {@inheritdoc}
77
     */
78
    public function applyToCollection(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, string $operationName = null)
79
    {
80
        $options = [];
81
82
        if (null !== $operationName) {
83
            $options = ['collection_operation_name' => $operationName];
84
        }
85
86
        $forceEager = $this->isForceEager($resourceClass, $options);
87
        $groups = $this->getSerializerGroups($resourceClass, $options, 'normalization_context');
88
89
        $this->joinRelations($queryBuilder, $resourceClass, $forceEager, $groups);
90
    }
91
92
    /**
93
     * {@inheritdoc}
94
     * The context may contain serialization groups which helps defining joined entities that are readable.
95
     */
96
    public function applyToItem(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, array $identifiers, string $operationName = null, array $context = [])
97
    {
98
        $options = [];
99
100
        if (null !== $operationName) {
101
            $options = ['item_operation_name' => $operationName];
102
        }
103
104
        $forceEager = $this->isForceEager($resourceClass, $options);
105
106
        if (isset($context['groups'])) {
107
            $groups = ['serializer_groups' => $context['groups']];
108
        } elseif (isset($context['resource_class'])) {
109
            $groups = $this->getSerializerGroups($context['resource_class'], $options, isset($context['api_denormalize']) ? 'denormalization_context' : 'normalization_context');
110
        } else {
111
            $groups = $this->getSerializerGroups($resourceClass, $options, 'normalization_context');
112
        }
113
114
        $this->joinRelations($queryBuilder, $resourceClass, $forceEager, $groups);
115
    }
116
117
    /**
118
     * Joins relations to eager load.
119
     *
120
     * @param QueryBuilder $queryBuilder
121
     * @param string       $resourceClass
122
     * @param bool         $forceEager
123
     * @param array        $propertyMetadataOptions
124
     * @param string       $originAlias             the current entity alias (first o, then a1, a2 etc.)
125
     * @param string       $relationAlias           the previous relation alias to keep it unique
126
     * @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
127
     * @param int          $joinCount               the number of joins
128
     *
129
     * @throws RuntimeException when the max number of joins has been reached
130
     */
131
    private function joinRelations(QueryBuilder $queryBuilder, string $resourceClass, bool $forceEager, array $propertyMetadataOptions = [], string $originAlias = 'o', string &$relationAlias = 'a', bool $wasLeftJoin = false, int &$joinCount = 0)
132
    {
133
        if ($joinCount > $this->maxJoins) {
134
            throw new RuntimeException('The total number of joined relations has exceeded the specified maximum. Raise the limit if necessary.');
135
        }
136
137
        $entityManager = $queryBuilder->getEntityManager();
138
        $classMetadata = $entityManager->getClassMetadata($resourceClass);
139
        $i = $j = 0;
140
141
        foreach ($classMetadata->associationMappings as $association => $mapping) {
142
            $propertyMetadata = $this->propertyMetadataFactory->create($resourceClass, $association, $propertyMetadataOptions);
143
144
            if (false === $forceEager && ClassMetadataInfo::FETCH_EAGER !== $mapping['fetch']) {
145
                continue;
146
            }
147
148
            if (false === $propertyMetadata->isReadableLink() || false === $propertyMetadata->isReadable()) {
149
                continue;
150
            }
151
152
            $joinColumns = $mapping['joinColumns'] ?? $mapping['joinTable']['joinColumns'] ?? null;
153
            if (false !== $wasLeftJoin || !isset($joinColumns[0]['nullable']) || false !== $joinColumns[0]['nullable']) {
154
                $method = 'leftJoin';
155
            } else {
156
                $method = 'innerJoin';
157
            }
158
159
            $associationAlias = $relationAlias.$i++;
160
            $queryBuilder->{$method}($originAlias.'.'.$association, $associationAlias);
161
            ++$joinCount;
162
163
            $relationAlias .= ++$j;
164
165
            $this->joinRelations($queryBuilder, $mapping['targetEntity'], $forceEager, $propertyMetadataOptions, $associationAlias, $relationAlias, $method === 'leftJoin', $joinCount);
166
        }
167
    }
168
169
    /**
170
     * Does an operation force eager?
171
     *
172
     * @param string $resourceClass
173
     * @param array  $options
174
     *
175
     * @return bool
176
     */
177
    private function isForceEager(string $resourceClass, array $options): bool
178
    {
179
        $resourceMetadata = $this->resourceMetadataFactory->create($resourceClass);
180
181
        if (isset($options['collection_operation_name'])) {
182
            $forceEager = $resourceMetadata->getCollectionOperationAttribute($options['collection_operation_name'], 'force_eager', null, true);
183
        } elseif (isset($options['item_operation_name'])) {
184
            $forceEager = $resourceMetadata->getItemOperationAttribute($options['item_operation_name'], 'force_eager', null, true);
185
        } else {
186
            $forceEager = $resourceMetadata->getAttribute('force_eager');
187
        }
188
189
        return is_bool($forceEager) ? $forceEager : $this->forceEager;
190
    }
191
}
192