Completed
Pull Request — master (#822)
by Antoine
03:19
created

EagerLoadingExtension::getSerializerGroups()   A

Complexity

Conditions 4
Paths 6

Size

Total Lines 18
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

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