Completed
Pull Request — master (#812)
by Antoine
07:01 queued 03:14
created

EagerLoadingExtension::joinRelations()   C

Complexity

Conditions 13
Paths 19

Size

Total Lines 57
Code Lines 34

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 57
rs 6.5962
c 0
b 0
f 0
cc 13
eloc 34
nc 19
nop 7

How to fix   Long Method    Complexity   

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:

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\Metadata\Property\Factory\PropertyMetadataFactoryInterface;
16
use ApiPlatform\Core\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface;
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
 */
28
final class EagerLoadingExtension implements QueryCollectionExtensionInterface, QueryItemExtensionInterface
29
{
30
    private $propertyNameCollectionFactory;
31
    private $propertyMetadataFactory;
32
    private $resourceMetadataFactory;
33
    private $maxJoins;
34
35
    public function __construct(PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, ResourceMetadataFactoryInterface $resourceMetadataFactory, int $maxJoins = 30)
36
    {
37
        $this->propertyMetadataFactory = $propertyMetadataFactory;
38
        $this->propertyNameCollectionFactory = $propertyNameCollectionFactory;
39
        $this->resourceMetadataFactory = $resourceMetadataFactory;
40
        $this->maxJoins = $maxJoins;
41
    }
42
43
    /**
44
     * Gets serializer groups once if available, if not it returns the $options array.
45
     *
46
     * @param array  $options       represents the operation name so that groups are the one of the specific operation
47
     * @param string $resourceClass
48
     * @param string $context       normalization_context or denormalization_context
49
     *
50
     * @return string[]
51
     */
52
    private function getSerializerGroups(string $resourceClass, array $options, string $context): array
53
    {
54
        $resourceMetadata = $this->resourceMetadataFactory->create($resourceClass);
55
56
        if (isset($options['collection_operation_name'])) {
57
            $context = $resourceMetadata->getCollectionOperationAttribute($options['collection_operation_name'], $context, null, true);
58
        } elseif (isset($options['item_operation_name'])) {
59
            $context = $resourceMetadata->getItemOperationAttribute($options['item_operation_name'], $context, null, true);
60
        } else {
61
            $context = $resourceMetadata->getAttribute($context);
62
        }
63
64
        if (empty($context['groups'])) {
65
            return $options;
66
        } else {
67
            return ['serializer_groups' => $context['groups']];
68
        }
69
    }
70
71
    /**
72
     * {@inheritdoc}
73
     */
74
    public function applyToCollection(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, string $operationName = null)
75
    {
76
        $options = [];
77
78
        if (null !== $operationName) {
79
            $options = ['collection_operation_name' => $operationName];
80
        }
81
82
        $groups = $this->getSerializerGroups($resourceClass, $options, 'normalization_context');
83
84
        $this->joinRelations($queryBuilder, $resourceClass, $groups);
85
    }
86
87
    /**
88
     * {@inheritdoc}
89
     */
90
    public function applyToItem(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, array $identifiers, string $operationName = null, array $context = [])
91
    {
92
        $options = [];
93
94
        if (null !== $operationName) {
95
            $options = ['item_operation_name' => $operationName];
96
        }
97
98
        if (isset($context['groups'])) {
99
            $groups = ['serializer_groups' => $context['groups']];
100
        } elseif (isset($context['resource_class'])) {
101
            $groups = $this->getSerializerGroups($context['resource_class'], $options, isset($context['api_denormalize']) ? 'denormalization_context' : 'normalization_context');
102
        } else {
103
            $groups = $this->getSerializerGroups($resourceClass, $options, 'normalization_context');
104
        }
105
106
        $this->joinRelations($queryBuilder, $resourceClass, $groups);
107
    }
108
109
    /**
110
     * Left joins relations to eager load.
111
     *
112
     * @param QueryBuilder $queryBuilder
113
     * @param string       $resourceClass
114
     * @param array        $propertyMetadataOptions
115
     * @param string       $originAlias             the current entity alias (first o, then a1, a2 etc.)
116
     * @param string       $relationAlias           the previous relation alias to keep it unique
117
     * @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
118
     * @param int          $joinsNumber             number of joins
119
     */
120
    private function joinRelations(QueryBuilder $queryBuilder, string $resourceClass, array $propertyMetadataOptions = [], string $originAlias = 'o', string &$relationAlias = 'a', bool $wasLeftJoin = false, $joinsNumber = 0)
121
    {
122
        if ($joinsNumber > $this->maxJoins) {
123
            throw new \RuntimeException('Too many joined relations, might be an infinite loop! Check out serialization groups!');
124
        }
125
126
        $entityManager = $queryBuilder->getEntityManager();
127
        $classMetadata = $entityManager->getClassMetadata($resourceClass);
128
        $j = 0;
129
        $i = 0;
130
131
        foreach ($classMetadata->associationMappings as $association => $mapping) {
132
            $propertyMetadata = $this->propertyMetadataFactory->create($resourceClass, $association, $propertyMetadataOptions);
133
134
            if (ClassMetadataInfo::FETCH_EAGER !== $mapping['fetch'] || false === $propertyMetadata->isReadableLink() || false === $propertyMetadata->isReadable()) {
135
                continue;
136
            }
137
138
            if (false === $wasLeftJoin) {
139
                $joinColumns = $mapping['joinColumns'] ?? $mapping['joinTable']['joinColumns'] ?? null;
140
141
                if (null === $joinColumns) {
142
                    $method = 'leftJoin';
143
                } else {
144
                    $method = false === $joinColumns[0]['nullable'] ? 'innerJoin' : 'leftJoin';
145
                }
146
            } else {
147
                $method = 'leftJoin';
148
            }
149
150
            $associationAlias = $relationAlias.$i++;
151
            $queryBuilder->{$method}($originAlias.'.'.$association, $associationAlias);
152
            $joinsNumber++;
153
            $select = [];
154
            $targetClassMetadata = $entityManager->getClassMetadata($mapping['targetEntity']);
155
156
            foreach ($this->propertyNameCollectionFactory->create($mapping['targetEntity']) as $property) {
157
                $propertyMetadata = $this->propertyMetadataFactory->create($mapping['targetEntity'], $property, $propertyMetadataOptions);
158
159
                if (true === $propertyMetadata->isIdentifier()) {
160
                    $select[] = $property;
161
                    continue;
162
                }
163
164
                //the field test allows to add methods to a Resource which do not reflect real database fields
165
                if (true === $targetClassMetadata->hasField($property) && true === $propertyMetadata->isReadable()) {
166
                    $select[] = $property;
167
                }
168
            }
169
170
            $queryBuilder->addSelect(sprintf('partial %s.{%s}', $associationAlias, implode(',', $select)));
171
172
            $relationAlias = $relationAlias.++$j;
173
174
            $this->joinRelations($queryBuilder, $mapping['targetEntity'], $propertyMetadataOptions, $associationAlias, $relationAlias, $method === 'leftJoin', $joinsNumber);
175
        }
176
    }
177
}
178