Completed
Pull Request — master (#855)
by Antoine
02:58
created

EagerLoadingExtension::joinRelations()   C

Complexity

Conditions 12
Paths 8

Size

Total Lines 42
Code Lines 25

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 42
rs 5.1612
c 0
b 0
f 0
cc 12
eloc 25
nc 8
nop 8

How to fix   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
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\Resource\Factory\ResourceMetadataFactoryInterface;
18
use Doctrine\ORM\Mapping\ClassMetadataInfo;
19
use Doctrine\ORM\QueryBuilder;
20
21
/**
22
 * Eager loads relations.
23
 *
24
 * @author Charles Sarrazin <[email protected]>
25
 * @author Kévin Dunglas <[email protected]>
26
 * @author Antoine Bluchet <[email protected]>
27
 * @author Baptiste Meyer <[email protected]>
28
 */
29
final class EagerLoadingExtension implements QueryCollectionExtensionInterface, QueryItemExtensionInterface
30
{
31
    private $propertyMetadataFactory;
32
    private $resourceMetadataFactory;
33
    private $maxJoins;
34
    private $forceEager;
35
36
    public function __construct(PropertyMetadataFactoryInterface $propertyMetadataFactory, ResourceMetadataFactoryInterface $resourceMetadataFactory, int $maxJoins = 30, bool $forceEager = true)
37
    {
38
        $this->propertyMetadataFactory = $propertyMetadataFactory;
39
        $this->resourceMetadataFactory = $resourceMetadataFactory;
40
        $this->maxJoins = $maxJoins;
41
        $this->forceEager = $forceEager;
42
    }
43
44
    /**
45
     * {@inheritdoc}
46
     */
47
    public function applyToCollection(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, string $operationName = null)
48
    {
49
        $options = [];
50
51
        if (null !== $operationName) {
52
            $options = ['collection_operation_name' => $operationName];
53
        }
54
55
        $forceEager = $this->isForceEager($resourceClass, $options);
56
57
        try {
58
            $groups = $this->getSerializerGroups($resourceClass, $options, 'normalization_context');
59
60
            $this->joinRelations($queryBuilder, $queryNameGenerator, $resourceClass, $forceEager, $queryBuilder->getRootAliases()[0], $groups);
61
        } catch(ResourceClassNotFoundException $resourceClassNotFoundException) {
0 ignored issues
show
Bug introduced by
The class ApiPlatform\Core\Bridge\...eClassNotFoundException does not exist. Did you forget a USE statement, or did you not list all dependencies?

Scrutinizer analyzes your composer.json/composer.lock file if available to determine the classes, and functions that are defined by your dependencies.

It seems like the listed class was neither found in your dependencies, nor was it found in the analyzed files in your repository. If you are using some other form of dependency management, you might want to disable this analysis.

Loading history...
62
            //ignore the not found exception
63
        }
64
    }
65
66
    /**
67
     * {@inheritdoc}
68
     * The context may contain serialization groups which helps defining joined entities that are readable.
69
     */
70
    public function applyToItem(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, array $identifiers, string $operationName = null, array $context = [])
71
    {
72
        $options = [];
73
74
        if (null !== $operationName) {
75
            $options = ['item_operation_name' => $operationName];
76
        }
77
78
        $forceEager = $this->isForceEager($resourceClass, $options);
79
80
        if (isset($context['groups'])) {
81
            $groups = ['serializer_groups' => $context['groups']];
82
        } elseif (isset($context['resource_class'])) {
83
            $groups = $this->getSerializerGroups($context['resource_class'], $options, isset($context['api_denormalize']) ? 'denormalization_context' : 'normalization_context');
84
        } else {
85
            $groups = $this->getSerializerGroups($resourceClass, $options, 'normalization_context');
86
        }
87
88
        $this->joinRelations($queryBuilder, $queryNameGenerator, $resourceClass, $forceEager, $queryBuilder->getRootAliases()[0], $groups);
89
    }
90
91
    /**
92
     * Joins relations to eager load.
93
     *
94
     * @param QueryBuilder                $queryBuilder
95
     * @param QueryNameGeneratorInterface $queryNameGenerator
96
     * @param string                      $resourceClass
97
     * @param bool                        $forceEager
98
     * @param string                      $parentAlias
99
     * @param array                       $propertyMetadataOptions
100
     * @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
101
     * @param int                         $joinCount               the number of joins
102
     *
103
     * @throws RuntimeException when the max number of joins has been reached
104
     */
105
    private function joinRelations(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, bool $forceEager, string $parentAlias, array $propertyMetadataOptions = [], bool $wasLeftJoin = false, int &$joinCount = 0)
106
    {
107
        if ($joinCount > $this->maxJoins) {
108
            throw new RuntimeException('The total number of joined relations has exceeded the specified maximum. Raise the limit if necessary.');
109
        }
110
111
        $entityManager = $queryBuilder->getEntityManager();
112
        $classMetadata = $entityManager->getClassMetadata($resourceClass);
113
114
        foreach ($classMetadata->associationMappings as $association => $mapping) {
115
            try {
116
                $propertyMetadata = $this->propertyMetadataFactory->create($resourceClass, $association, $propertyMetadataOptions);
117
            } catch (PropertyNotFoundException $propertyNotFoundException) {
0 ignored issues
show
Bug introduced by
The class ApiPlatform\Core\Bridge\...opertyNotFoundException does not exist. Did you forget a USE statement, or did you not list all dependencies?

Scrutinizer analyzes your composer.json/composer.lock file if available to determine the classes, and functions that are defined by your dependencies.

It seems like the listed class was neither found in your dependencies, nor was it found in the analyzed files in your repository. If you are using some other form of dependency management, you might want to disable this analysis.

Loading history...
118
                //skip properties not found
119
                continue;
120
            } catch (ResourceClassNotFoundException $resourceClassNotFoundException) {
0 ignored issues
show
Bug introduced by
The class ApiPlatform\Core\Bridge\...eClassNotFoundException does not exist. Did you forget a USE statement, or did you not list all dependencies?

Scrutinizer analyzes your composer.json/composer.lock file if available to determine the classes, and functions that are defined by your dependencies.

It seems like the listed class was neither found in your dependencies, nor was it found in the analyzed files in your repository. If you are using some other form of dependency management, you might want to disable this analysis.

Loading history...
121
                //skip associations that are not resource classes
122
                continue;
123
            }
124
125
            if (false === $forceEager && ClassMetadataInfo::FETCH_EAGER !== $mapping['fetch']) {
126
                continue;
127
            }
128
129
            if (false === $propertyMetadata->isReadableLink() || false === $propertyMetadata->isReadable()) {
130
                continue;
131
            }
132
133
            $joinColumns = $mapping['joinColumns'] ?? $mapping['joinTable']['joinColumns'] ?? null;
134
            if (false !== $wasLeftJoin || !isset($joinColumns[0]['nullable']) || false !== $joinColumns[0]['nullable']) {
135
                $method = 'leftJoin';
136
            } else {
137
                $method = 'innerJoin';
138
            }
139
140
            $associationAlias = $queryNameGenerator->generateJoinAlias($association);
141
            $queryBuilder->{$method}(sprintf('%s.%s', $parentAlias, $association), $associationAlias);
142
            ++$joinCount;
143
144
            $this->joinRelations($queryBuilder, $queryNameGenerator, $mapping['targetEntity'], $forceEager, $associationAlias, $propertyMetadataOptions, $method === 'leftJoin', $joinCount);
145
        }
146
    }
147
148
    /**
149
     * Gets serializer groups if available, if not it returns the $options array.
150
     *
151
     * @param string $resourceClass
152
     * @param array  $options       represents the operation name so that groups are the one of the specific operation
153
     * @param string $context       normalization_context or denormalization_context
154
     *
155
     * @return array
156
     */
157
    private function getSerializerGroups(string $resourceClass, array $options, string $context): array
158
    {
159
        $resourceMetadata = $this->resourceMetadataFactory->create($resourceClass);
160
161
        if (isset($options['collection_operation_name'])) {
162
            $context = $resourceMetadata->getCollectionOperationAttribute($options['collection_operation_name'], $context, null, true);
163
        } elseif (isset($options['item_operation_name'])) {
164
            $context = $resourceMetadata->getItemOperationAttribute($options['item_operation_name'], $context, null, true);
165
        } else {
166
            $context = $resourceMetadata->getAttribute($context);
167
        }
168
169
        if (empty($context['groups'])) {
170
            return $options;
171
        }
172
173
        return ['serializer_groups' => $context['groups']];
174
    }
175
176
    /**
177
     * Does an operation force eager?
178
     *
179
     * @param string $resourceClass
180
     * @param array  $options
181
     *
182
     * @return bool
183
     */
184
    private function isForceEager(string $resourceClass, array $options): bool
185
    {
186
        $resourceMetadata = $this->resourceMetadataFactory->create($resourceClass);
187
188
        if (isset($options['collection_operation_name'])) {
189
            $forceEager = $resourceMetadata->getCollectionOperationAttribute($options['collection_operation_name'], 'force_eager', null, true);
190
        } elseif (isset($options['item_operation_name'])) {
191
            $forceEager = $resourceMetadata->getItemOperationAttribute($options['item_operation_name'], 'force_eager', null, true);
192
        } else {
193
            $forceEager = $resourceMetadata->getAttribute('force_eager');
194
        }
195
196
        return is_bool($forceEager) ? $forceEager : $this->forceEager;
197
    }
198
}
199