Completed
Pull Request — master (#855)
by Antoine
02:54
created

EagerLoadingExtension::applyToItem()   B

Complexity

Conditions 5
Paths 6

Size

Total Lines 20
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 20
rs 8.8571
c 0
b 0
f 0
cc 5
eloc 12
nc 6
nop 6
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