Completed
Push — master ( 84fd0d...49f1b0 )
by Kévin
11:29 queued 01:11
created

EagerLoadingExtension::joinRelations()   C

Complexity

Conditions 12
Paths 10

Size

Total Lines 48
Code Lines 29

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 48
rs 5.1266
c 0
b 0
f 0
cc 12
eloc 29
nc 10
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\Property\Factory\PropertyNameCollectionFactoryInterface;
20
use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface;
21
use Doctrine\ORM\Mapping\ClassMetadataInfo;
22
use Doctrine\ORM\QueryBuilder;
23
24
/**
25
 * Eager loads relations.
26
 *
27
 * @author Charles Sarrazin <[email protected]>
28
 * @author Kévin Dunglas <[email protected]>
29
 * @author Antoine Bluchet <[email protected]>
30
 * @author Baptiste Meyer <[email protected]>
31
 */
32
final class EagerLoadingExtension implements QueryCollectionExtensionInterface, QueryItemExtensionInterface
33
{
34
    private $propertyMetadataFactory;
35
    private $resourceMetadataFactory;
36
    private $maxJoins;
37
    private $forceEager;
38
39 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...
40
    {
41
        $this->propertyNameCollectionFactory = $propertyNameCollectionFactory;
0 ignored issues
show
Bug introduced by
The property propertyNameCollectionFactory does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
42
        $this->propertyMetadataFactory = $propertyMetadataFactory;
43
        $this->resourceMetadataFactory = $resourceMetadataFactory;
44
        $this->maxJoins = $maxJoins;
45
        $this->forceEager = $forceEager;
46
    }
47
48
    /**
49
     * {@inheritdoc}
50
     */
51
    public function applyToCollection(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, string $operationName = null)
52
    {
53
        $options = [];
54
55
        if (null !== $operationName) {
56
            $options = ['collection_operation_name' => $operationName];
57
        }
58
59
        $forceEager = $this->isForceEager($resourceClass, $options);
60
61
        try {
62
            $groups = $this->getSerializerGroups($resourceClass, $options, 'normalization_context');
63
64
            $this->joinRelations($queryBuilder, $queryNameGenerator, $resourceClass, $forceEager, $queryBuilder->getRootAliases()[0], $groups);
65
        } catch (ResourceClassNotFoundException $resourceClassNotFoundException) {
66
            //ignore the not found exception
67
        }
68
    }
69
70
    /**
71
     * {@inheritdoc}
72
     * The context may contain serialization groups which helps defining joined entities that are readable.
73
     */
74
    public function applyToItem(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, array $identifiers, string $operationName = null, array $context = [])
75
    {
76
        $options = [];
77
78
        if (null !== $operationName) {
79
            $options = ['item_operation_name' => $operationName];
80
        }
81
82
        $forceEager = $this->isForceEager($resourceClass, $options);
83
84
        if (isset($context['groups'])) {
85
            $groups = ['serializer_groups' => $context['groups']];
86
        } elseif (isset($context['resource_class'])) {
87
            $groups = $this->getSerializerGroups($context['resource_class'], $options, isset($context['api_denormalize']) ? 'denormalization_context' : 'normalization_context');
88
        } else {
89
            $groups = $this->getSerializerGroups($resourceClass, $options, 'normalization_context');
90
        }
91
92
        $this->joinRelations($queryBuilder, $queryNameGenerator, $resourceClass, $forceEager, $queryBuilder->getRootAliases()[0], $groups);
93
    }
94
95
    /**
96
     * Joins relations to eager load.
97
     *
98
     * @param QueryBuilder                $queryBuilder
99
     * @param QueryNameGeneratorInterface $queryNameGenerator
100
     * @param string                      $resourceClass
101
     * @param bool                        $forceEager
102
     * @param string                      $parentAlias
103
     * @param array                       $propertyMetadataOptions
104
     * @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
105
     * @param int                         $joinCount               the number of joins
106
     *
107
     * @throws RuntimeException when the max number of joins has been reached
108
     */
109
    private function joinRelations(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, bool $forceEager, string $parentAlias, array $propertyMetadataOptions = [], bool $wasLeftJoin = false, int &$joinCount = 0)
110
    {
111
        if ($joinCount > $this->maxJoins) {
112
            throw new RuntimeException('The total number of joined relations has exceeded the specified maximum. Raise the limit if necessary.');
113
        }
114
115
        $entityManager = $queryBuilder->getEntityManager();
116
        $classMetadata = $entityManager->getClassMetadata($resourceClass);
117
118
        foreach ($classMetadata->associationMappings as $association => $mapping) {
119
            try {
120
                $propertyMetadata = $this->propertyMetadataFactory->create($resourceClass, $association, $propertyMetadataOptions);
121
            } catch (PropertyNotFoundException $propertyNotFoundException) {
122
                //skip properties not found
123
                continue;
124
            } catch (ResourceClassNotFoundException $resourceClassNotFoundException) {
125
                //skip associations that are not resource classes
126
                continue;
127
            }
128
129
            if (false === $forceEager && ClassMetadataInfo::FETCH_EAGER !== $mapping['fetch']) {
130
                continue;
131
            }
132
133
            if (false === $propertyMetadata->isReadableLink() || false === $propertyMetadata->isReadable()) {
134
                continue;
135
            }
136
137
            $isNullable = $mapping['joinColumns'][0]['nullable'] ?? true;
138
            if (false !== $wasLeftJoin || true === $isNullable) {
139
                $method = 'leftJoin';
140
            } else {
141
                $method = 'innerJoin';
142
            }
143
144
            $associationAlias = $queryNameGenerator->generateJoinAlias($association);
145
            $queryBuilder->{$method}(sprintf('%s.%s', $parentAlias, $association), $associationAlias);
146
            ++$joinCount;
147
148
            try {
149
                $this->addSelect($queryBuilder, $mapping['targetEntity'], $associationAlias, $propertyMetadataOptions);
150
            } catch (ResourceClassNotFoundException $resourceClassNotFoundException) {
151
                continue;
152
            }
153
154
            $this->joinRelations($queryBuilder, $queryNameGenerator, $mapping['targetEntity'], $forceEager, $associationAlias, $propertyMetadataOptions, $method === 'leftJoin', $joinCount);
155
        }
156
    }
157
158
    private function addSelect(QueryBuilder $queryBuilder, string $entity, string $associationAlias, array $propertyMetadataOptions)
159
    {
160
        $select = [];
161
        $entityManager = $queryBuilder->getEntityManager();
162
        $targetClassMetadata = $entityManager->getClassMetadata($entity);
163
164
        foreach ($this->propertyNameCollectionFactory->create($entity) as $property) {
165
            $propertyMetadata = $this->propertyMetadataFactory->create($entity, $property, $propertyMetadataOptions);
166
167
            if (true === $propertyMetadata->isIdentifier()) {
168
                $select[] = $property;
169
                continue;
170
            }
171
172
            //the field test allows to add methods to a Resource which do not reflect real database fields
173
            if (true === $targetClassMetadata->hasField($property) && true === $propertyMetadata->isReadable()) {
174
                $select[] = $property;
175
            }
176
        }
177
178
        $queryBuilder->addSelect(sprintf('partial %s.{%s}', $associationAlias, implode(',', $select)));
179
    }
180
181
    /**
182
     * Gets serializer groups if available, if not it returns the $options array.
183
     *
184
     * @param string $resourceClass
185
     * @param array  $options       represents the operation name so that groups are the one of the specific operation
186
     * @param string $context       normalization_context or denormalization_context
187
     *
188
     * @return array
189
     */
190
    private function getSerializerGroups(string $resourceClass, array $options, string $context): array
191
    {
192
        $resourceMetadata = $this->resourceMetadataFactory->create($resourceClass);
193
194
        if (isset($options['collection_operation_name'])) {
195
            $context = $resourceMetadata->getCollectionOperationAttribute($options['collection_operation_name'], $context, null, true);
196
        } elseif (isset($options['item_operation_name'])) {
197
            $context = $resourceMetadata->getItemOperationAttribute($options['item_operation_name'], $context, null, true);
198
        } else {
199
            $context = $resourceMetadata->getAttribute($context);
200
        }
201
202
        if (empty($context['groups'])) {
203
            return $options;
204
        }
205
206
        return ['serializer_groups' => $context['groups']];
207
    }
208
209
    /**
210
     * Does an operation force eager?
211
     *
212
     * @param string $resourceClass
213
     * @param array  $options
214
     *
215
     * @return bool
216
     */
217
    private function isForceEager(string $resourceClass, array $options): bool
218
    {
219
        $resourceMetadata = $this->resourceMetadataFactory->create($resourceClass);
220
221
        if (isset($options['collection_operation_name'])) {
222
            $forceEager = $resourceMetadata->getCollectionOperationAttribute($options['collection_operation_name'], 'force_eager', null, true);
223
        } elseif (isset($options['item_operation_name'])) {
224
            $forceEager = $resourceMetadata->getItemOperationAttribute($options['item_operation_name'], 'force_eager', null, true);
225
        } else {
226
            $forceEager = $resourceMetadata->getAttribute('force_eager');
227
        }
228
229
        return is_bool($forceEager) ? $forceEager : $this->forceEager;
230
    }
231
}
232