Completed
Push — master ( 62f26d...0357a4 )
by Kévin
12s
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\PropertyNotFoundException;
16
use ApiPlatform\Core\Exception\ResourceClassNotFoundException;
17
use ApiPlatform\Core\Exception\RuntimeException;
18
use ApiPlatform\Core\Metadata\Property\Factory\PropertyMetadataFactoryInterface;
19
use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface;
20
use Doctrine\ORM\Mapping\ClassMetadataInfo;
21
use Doctrine\ORM\QueryBuilder;
22
23
/**
24
 * Eager loads relations.
25
 *
26
 * @author Charles Sarrazin <[email protected]>
27
 * @author Kévin Dunglas <[email protected]>
28
 * @author Antoine Bluchet <[email protected]>
29
 * @author Baptiste Meyer <[email protected]>
30
 */
31
final class EagerLoadingExtension implements QueryCollectionExtensionInterface, QueryItemExtensionInterface
32
{
33
    private $propertyMetadataFactory;
34
    private $resourceMetadataFactory;
35
    private $maxJoins;
36
    private $forceEager;
37
38
    public function __construct(PropertyMetadataFactoryInterface $propertyMetadataFactory, ResourceMetadataFactoryInterface $resourceMetadataFactory, int $maxJoins = 30, bool $forceEager = true)
39
    {
40
        $this->propertyMetadataFactory = $propertyMetadataFactory;
41
        $this->resourceMetadataFactory = $resourceMetadataFactory;
42
        $this->maxJoins = $maxJoins;
43
        $this->forceEager = $forceEager;
44
    }
45
46
    /**
47
     * {@inheritdoc}
48
     */
49
    public function applyToCollection(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, string $operationName = null)
50
    {
51
        $options = [];
52
53
        if (null !== $operationName) {
54
            $options = ['collection_operation_name' => $operationName];
55
        }
56
57
        $forceEager = $this->isForceEager($resourceClass, $options);
58
59
        try {
60
            $groups = $this->getSerializerGroups($resourceClass, $options, 'normalization_context');
61
62
            $this->joinRelations($queryBuilder, $queryNameGenerator, $resourceClass, $forceEager, $queryBuilder->getRootAliases()[0], $groups);
63
        } catch (ResourceClassNotFoundException $resourceClassNotFoundException) {
64
            //ignore the not found exception
65
        }
66
    }
67
68
    /**
69
     * {@inheritdoc}
70
     * The context may contain serialization groups which helps defining joined entities that are readable.
71
     */
72
    public function applyToItem(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, array $identifiers, string $operationName = null, array $context = [])
73
    {
74
        $options = [];
75
76
        if (null !== $operationName) {
77
            $options = ['item_operation_name' => $operationName];
78
        }
79
80
        $forceEager = $this->isForceEager($resourceClass, $options);
81
82
        if (isset($context['groups'])) {
83
            $groups = ['serializer_groups' => $context['groups']];
84
        } elseif (isset($context['resource_class'])) {
85
            $groups = $this->getSerializerGroups($context['resource_class'], $options, isset($context['api_denormalize']) ? 'denormalization_context' : 'normalization_context');
86
        } else {
87
            $groups = $this->getSerializerGroups($resourceClass, $options, 'normalization_context');
88
        }
89
90
        $this->joinRelations($queryBuilder, $queryNameGenerator, $resourceClass, $forceEager, $queryBuilder->getRootAliases()[0], $groups);
91
    }
92
93
    /**
94
     * Joins relations to eager load.
95
     *
96
     * @param QueryBuilder                $queryBuilder
97
     * @param QueryNameGeneratorInterface $queryNameGenerator
98
     * @param string                      $resourceClass
99
     * @param bool                        $forceEager
100
     * @param string                      $parentAlias
101
     * @param array                       $propertyMetadataOptions
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
     *
105
     * @throws RuntimeException when the max number of joins has been reached
106
     */
107
    private function joinRelations(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, bool $forceEager, string $parentAlias, array $propertyMetadataOptions = [], bool $wasLeftJoin = false, int &$joinCount = 0)
108
    {
109
        if ($joinCount > $this->maxJoins) {
110
            throw new RuntimeException('The total number of joined relations has exceeded the specified maximum. Raise the limit if necessary.');
111
        }
112
113
        $entityManager = $queryBuilder->getEntityManager();
114
        $classMetadata = $entityManager->getClassMetadata($resourceClass);
115
116
        foreach ($classMetadata->associationMappings as $association => $mapping) {
117
            try {
118
                $propertyMetadata = $this->propertyMetadataFactory->create($resourceClass, $association, $propertyMetadataOptions);
119
            } catch (PropertyNotFoundException $propertyNotFoundException) {
120
                //skip properties not found
121
                continue;
122
            } catch (ResourceClassNotFoundException $resourceClassNotFoundException) {
123
                //skip associations that are not resource classes
124
                continue;
125
            }
126
127
            if (false === $forceEager && ClassMetadataInfo::FETCH_EAGER !== $mapping['fetch']) {
128
                continue;
129
            }
130
131
            if (false === $propertyMetadata->isReadableLink() || false === $propertyMetadata->isReadable()) {
132
                continue;
133
            }
134
135
            $joinColumns = $mapping['joinColumns'] ?? $mapping['joinTable']['joinColumns'] ?? null;
136
            if (false !== $wasLeftJoin || !isset($joinColumns[0]['nullable']) || false !== $joinColumns[0]['nullable']) {
137
                $method = 'leftJoin';
138
            } else {
139
                $method = 'innerJoin';
140
            }
141
142
            $associationAlias = $queryNameGenerator->generateJoinAlias($association);
143
            $queryBuilder->{$method}(sprintf('%s.%s', $parentAlias, $association), $associationAlias);
144
            ++$joinCount;
145
146
            $this->joinRelations($queryBuilder, $queryNameGenerator, $mapping['targetEntity'], $forceEager, $associationAlias, $propertyMetadataOptions, $method === 'leftJoin', $joinCount);
147
        }
148
    }
149
150
    /**
151
     * Gets serializer groups if available, if not it returns the $options array.
152
     *
153
     * @param string $resourceClass
154
     * @param array  $options       represents the operation name so that groups are the one of the specific operation
155
     * @param string $context       normalization_context or denormalization_context
156
     *
157
     * @return array
158
     */
159
    private function getSerializerGroups(string $resourceClass, array $options, string $context): array
160
    {
161
        $resourceMetadata = $this->resourceMetadataFactory->create($resourceClass);
162
163
        if (isset($options['collection_operation_name'])) {
164
            $context = $resourceMetadata->getCollectionOperationAttribute($options['collection_operation_name'], $context, null, true);
165
        } elseif (isset($options['item_operation_name'])) {
166
            $context = $resourceMetadata->getItemOperationAttribute($options['item_operation_name'], $context, null, true);
167
        } else {
168
            $context = $resourceMetadata->getAttribute($context);
169
        }
170
171
        if (empty($context['groups'])) {
172
            return $options;
173
        }
174
175
        return ['serializer_groups' => $context['groups']];
176
    }
177
178
    /**
179
     * Does an operation force eager?
180
     *
181
     * @param string $resourceClass
182
     * @param array  $options
183
     *
184
     * @return bool
185
     */
186
    private function isForceEager(string $resourceClass, array $options): bool
187
    {
188
        $resourceMetadata = $this->resourceMetadataFactory->create($resourceClass);
189
190
        if (isset($options['collection_operation_name'])) {
191
            $forceEager = $resourceMetadata->getCollectionOperationAttribute($options['collection_operation_name'], 'force_eager', null, true);
192
        } elseif (isset($options['item_operation_name'])) {
193
            $forceEager = $resourceMetadata->getItemOperationAttribute($options['item_operation_name'], 'force_eager', null, true);
194
        } else {
195
            $forceEager = $resourceMetadata->getAttribute('force_eager');
196
        }
197
198
        return is_bool($forceEager) ? $forceEager : $this->forceEager;
199
    }
200
}
201