Completed
Pull Request — master (#822)
by Antoine
05:59
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\Exception\RuntimeException;
15
use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryNameGeneratorInterface;
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 = 100)
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
     *
120
     * @throws RuntimeException                     when the max number of joins has been reached
121
     */
122
    private function joinRelations(QueryBuilder $queryBuilder, string $resourceClass, array $propertyMetadataOptions = [], string $originAlias = 'o', string &$relationAlias = 'a', bool $wasLeftJoin = false, $depth = 0)
123
    {
124
        if ($depth > $this->maxJoins) {
125
            throw new RuntimeException('Too many joined relations, might be an infinite loop! Check out serialization groups!');
126
        }
127
128
        $entityManager = $queryBuilder->getEntityManager();
129
        $classMetadata = $entityManager->getClassMetadata($resourceClass);
130
        $j = 0;
131
        $i = 0;
132
133
        foreach ($classMetadata->associationMappings as $association => $mapping) {
134
            $propertyMetadata = $this->propertyMetadataFactory->create($resourceClass, $association, $propertyMetadataOptions);
135
136
            if (ClassMetadataInfo::FETCH_EAGER !== $mapping['fetch'] || false === $propertyMetadata->isReadableLink() || false === $propertyMetadata->isReadable()) {
137
                continue;
138
            }
139
140
            if (false === $wasLeftJoin) {
141
                $joinColumns = $mapping['joinColumns'] ?? $mapping['joinTable']['joinColumns'] ?? null;
142
143
                if (null === $joinColumns) {
144
                    $method = 'leftJoin';
145
                } else {
146
                    $method = false === $joinColumns[0]['nullable'] ? 'innerJoin' : 'leftJoin';
147
                }
148
            } else {
149
                $method = 'leftJoin';
150
            }
151
152
            $associationAlias = $relationAlias.$i++;
153
            $queryBuilder->{$method}($originAlias.'.'.$association, $associationAlias);
154
            $select = [];
155
            $targetClassMetadata = $entityManager->getClassMetadata($mapping['targetEntity']);
156
157
            foreach ($this->propertyNameCollectionFactory->create($mapping['targetEntity']) as $property) {
158
                $propertyMetadata = $this->propertyMetadataFactory->create($mapping['targetEntity'], $property, $propertyMetadataOptions);
159
160
                if (true === $propertyMetadata->isIdentifier()) {
161
                    $select[] = $property;
162
                    continue;
163
                }
164
165
                //the field test allows to add methods to a Resource which do not reflect real database fields
166
                if (true === $targetClassMetadata->hasField($property) && true === $propertyMetadata->isReadable()) {
167
                    $select[] = $property;
168
                }
169
            }
170
171
            $queryBuilder->addSelect(sprintf('partial %s.{%s}', $associationAlias, implode(',', $select)));
172
173
            $relationAlias .= ++$j;
174
175
            $this->joinRelations($queryBuilder, $mapping['targetEntity'], $propertyMetadataOptions, $associationAlias, $relationAlias, $method === 'leftJoin', ++$depth);
176
        }
177
    }
178
}
179