Skip to content

Commit b35c20b

Browse files
author
abluchet
committed
Disable eager loading partial fetch by default #1069
1 parent 69050d3 commit b35c20b

File tree

10 files changed

+132
-26
lines changed

10 files changed

+132
-26
lines changed

src/Bridge/Doctrine/Orm/Extension/EagerLoadingExtension.php

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -44,13 +44,17 @@ final class EagerLoadingExtension implements QueryCollectionExtensionInterface,
4444
private $serializerContextBuilder;
4545
private $requestStack;
4646

47-
public function __construct(PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, ResourceMetadataFactoryInterface $resourceMetadataFactory, int $maxJoins = 30, bool $forceEager = true, RequestStack $requestStack = null, SerializerContextBuilderInterface $serializerContextBuilder = null)
47+
/**
48+
* @TODO move $fetchPartial after $forceEager (@soyuka) in 3.0
49+
*/
50+
public function __construct(PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, ResourceMetadataFactoryInterface $resourceMetadataFactory, int $maxJoins = 30, bool $forceEager = true, RequestStack $requestStack = null, SerializerContextBuilderInterface $serializerContextBuilder = null, bool $fetchPartial = false)
4851
{
4952
$this->propertyNameCollectionFactory = $propertyNameCollectionFactory;
5053
$this->propertyMetadataFactory = $propertyMetadataFactory;
5154
$this->resourceMetadataFactory = $resourceMetadataFactory;
5255
$this->maxJoins = $maxJoins;
5356
$this->forceEager = $forceEager;
57+
$this->fetchPartial = $fetchPartial;
5458
$this->serializerContextBuilder = $serializerContextBuilder;
5559
$this->requestStack = $requestStack;
5660
}
@@ -67,10 +71,11 @@ public function applyToCollection(QueryBuilder $queryBuilder, QueryNameGenerator
6771
}
6872

6973
$forceEager = $this->shouldOperationForceEager($resourceClass, $options);
74+
$fetchPartial = $this->shouldOperationFetchPartial($resourceClass, $options);
7075

7176
$groups = $this->getSerializerGroups($resourceClass, $options, 'normalization_context');
7277

73-
$this->joinRelations($queryBuilder, $queryNameGenerator, $resourceClass, $forceEager, $queryBuilder->getRootAliases()[0], $groups);
78+
$this->joinRelations($queryBuilder, $queryNameGenerator, $resourceClass, $forceEager, $fetchPartial, $queryBuilder->getRootAliases()[0], $groups);
7479
}
7580

7681
/**
@@ -86,6 +91,7 @@ public function applyToItem(QueryBuilder $queryBuilder, QueryNameGeneratorInterf
8691
}
8792

8893
$forceEager = $this->shouldOperationForceEager($resourceClass, $options);
94+
$fetchPartial = $this->shouldOperationFetchPartial($resourceClass, $options);
8995

9096
if (isset($context['groups'])) {
9197
$groups = ['serializer_groups' => $context['groups']];
@@ -95,7 +101,7 @@ public function applyToItem(QueryBuilder $queryBuilder, QueryNameGeneratorInterf
95101
$groups = $this->getSerializerGroups($resourceClass, $options, 'normalization_context');
96102
}
97103

98-
$this->joinRelations($queryBuilder, $queryNameGenerator, $resourceClass, $forceEager, $queryBuilder->getRootAliases()[0], $groups);
104+
$this->joinRelations($queryBuilder, $queryNameGenerator, $resourceClass, $forceEager, $fetchPartial, $queryBuilder->getRootAliases()[0], $groups);
99105
}
100106

101107
/**
@@ -112,7 +118,7 @@ public function applyToItem(QueryBuilder $queryBuilder, QueryNameGeneratorInterf
112118
*
113119
* @throws RuntimeException when the max number of joins has been reached
114120
*/
115-
private function joinRelations(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, bool $forceEager, string $parentAlias, array $propertyMetadataOptions = [], bool $wasLeftJoin = false, int &$joinCount = 0)
121+
private function joinRelations(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, bool $forceEager, bool $fetchPartial, string $parentAlias, array $propertyMetadataOptions = [], bool $wasLeftJoin = false, int &$joinCount = 0)
116122
{
117123
if ($joinCount > $this->maxJoins) {
118124
throw new RuntimeException('The total number of joined relations has exceeded the specified maximum. Raise the limit if necessary.');
@@ -152,18 +158,23 @@ private function joinRelations(QueryBuilder $queryBuilder, QueryNameGeneratorInt
152158
$queryBuilder->{$method}(sprintf('%s.%s', $parentAlias, $association), $associationAlias);
153159
++$joinCount;
154160

155-
try {
156-
$this->addSelect($queryBuilder, $mapping['targetEntity'], $associationAlias, $propertyMetadataOptions);
157-
} catch (ResourceClassNotFoundException $resourceClassNotFoundException) {
158-
continue;
161+
if (true === $fetchPartial) {
162+
try {
163+
$this->addSelect($queryBuilder, $mapping['targetEntity'], $associationAlias, $propertyMetadataOptions);
164+
} catch (ResourceClassNotFoundException $resourceClassNotFoundException) {
165+
continue;
166+
}
167+
} else {
168+
$queryBuilder->addSelect($associationAlias);
159169
}
160170

171+
// Avoid recursion
161172
if ($mapping['targetEntity'] === $resourceClass) {
162173
$queryBuilder->addSelect($associationAlias);
163174
continue;
164175
}
165176

166-
$this->joinRelations($queryBuilder, $queryNameGenerator, $mapping['targetEntity'], $forceEager, $associationAlias, $propertyMetadataOptions, $method === 'leftJoin', $joinCount);
177+
$this->joinRelations($queryBuilder, $queryNameGenerator, $mapping['targetEntity'], $forceEager, $fetchPartial, $associationAlias, $propertyMetadataOptions, $method === 'leftJoin', $joinCount);
167178
}
168179
}
169180

@@ -184,7 +195,7 @@ private function addSelect(QueryBuilder $queryBuilder, string $entity, string $a
184195
}
185196

186197
//the field test allows to add methods to a Resource which do not reflect real database fields
187-
if (true === $targetClassMetadata->hasField($property) && true === $propertyMetadata->isReadable()) {
198+
if (true === $targetClassMetadata->hasField($property) && (true === $propertyMetadata->getAttribute('fetchable') || true === $propertyMetadata->isReadable())) {
188199
$select[] = $property;
189200
}
190201
}

src/Bridge/Doctrine/Orm/Util/EagerLoadingTrait.php

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
trait EagerLoadingTrait
2525
{
2626
private $forceEager;
27+
private $fetchPartial;
2728
private $resourceMetadataFactory;
2829

2930
/**
@@ -35,18 +36,46 @@ trait EagerLoadingTrait
3536
* @return bool
3637
*/
3738
private function shouldOperationForceEager(string $resourceClass, array $options): bool
39+
{
40+
return $this->getBooleanOperationAttribute($resourceClass, $options, 'force_eager', $this->forceEager);
41+
}
42+
43+
/**
44+
* Checks if an operation has a `fetch_partial` attribute.
45+
*
46+
* @param string $resourceClass
47+
* @param array $options
48+
*
49+
* @return bool
50+
*/
51+
private function shouldOperationFetchPartial(string $resourceClass, array $options): bool
52+
{
53+
return $this->getBooleanOperationAttribute($resourceClass, $options, 'fetch_partial', $this->fetchPartial);
54+
}
55+
56+
/**
57+
* Get the boolean attribute of an operation or the resource metadata.
58+
*
59+
* @param string $resourceClass
60+
* @param array $options
61+
* @param string $attributeName
62+
* @param bool $default
63+
*
64+
* @return bool
65+
*/
66+
private function getBooleanOperationAttribute(string $resourceClass, array $options, string $attributeName, bool $default): bool
3867
{
3968
$resourceMetadata = $this->resourceMetadataFactory->create($resourceClass);
4069

4170
if (isset($options['collection_operation_name'])) {
42-
$forceEager = $resourceMetadata->getCollectionOperationAttribute($options['collection_operation_name'], 'force_eager', null, true);
71+
$attribute = $resourceMetadata->getCollectionOperationAttribute($options['collection_operation_name'], $attributeName, null, true);
4372
} elseif (isset($options['item_operation_name'])) {
44-
$forceEager = $resourceMetadata->getItemOperationAttribute($options['item_operation_name'], 'force_eager', null, true);
73+
$attribute = $resourceMetadata->getItemOperationAttribute($options['item_operation_name'], $attributeName, null, true);
4574
} else {
46-
$forceEager = $resourceMetadata->getAttribute('force_eager');
75+
$attribute = $resourceMetadata->getAttribute($attributeName);
4776
}
4877

49-
return is_bool($forceEager) ? $forceEager : (bool) $this->forceEager;
78+
return is_bool($attribute) ? $attribute : $default;
5079
}
5180

5281
/**

src/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@ private function handleConfig(ContainerBuilder $container, array $config, array
110110
$container->setParameter('api_platform.error_formats', $errorFormats);
111111
$container->setParameter('api_platform.eager_loading.enabled', $config['eager_loading']['enabled']);
112112
$container->setParameter('api_platform.eager_loading.max_joins', $config['eager_loading']['max_joins']);
113+
$container->setParameter('api_platform.eager_loading.fetch_partial', $config['eager_loading']['fetch_partial']);
113114
$container->setParameter('api_platform.eager_loading.force_eager', $config['eager_loading']['force_eager']);
114115
$container->setParameter('api_platform.collection.order', $config['collection']['order']);
115116
$container->setParameter('api_platform.collection.order_parameter_name', $config['collection']['order_parameter_name']);

src/Bridge/Symfony/Bundle/DependencyInjection/Configuration.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ public function getConfigTreeBuilder()
5050
->addDefaultsIfNotSet()
5151
->children()
5252
->booleanNode('enabled')->defaultTrue()->info('To enable or disable eager loading')->end()
53+
->booleanNode('fetch_partial')->defaultFalse()->info('Fetch only partial data according to serialization groups. If enabled, Doctrine ORM entities will not work as expected if any of the other fields are used.')->end()
5354
->integerNode('max_joins')->defaultValue(30)->info('Max number of joined relations before EagerLoading throws a RuntimeException')->end()
5455
->booleanNode('force_eager')->defaultTrue()->info('Force join on every relation. If disabled, it will only join relations having the EAGER fetch mode.')->end()
5556
->end()

src/Bridge/Symfony/Bundle/Resources/config/doctrine_orm.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@
9393
<argument type="service" id="api_platform.metadata.resource.metadata_factory" />
9494
<argument>%api_platform.eager_loading.max_joins%</argument>
9595
<argument>%api_platform.eager_loading.force_eager%</argument>
96+
<argument>%api_platform.eager_loading.fetch_partial%</argument>
9697
<argument type="service" id="request_stack" />
9798
<argument type="service" id="api_platform.serializer.context_builder" />
9899

src/Metadata/Property/PropertyMetadata.php

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -288,6 +288,23 @@ public function getAttributes()
288288
return $this->attributes;
289289
}
290290

291+
/**
292+
* Gets an attribute.
293+
*
294+
* @param string $key
295+
* @param mixed $defaultValue
296+
*
297+
* @return mixed
298+
*/
299+
public function getAttribute(string $key, $defaultValue = null)
300+
{
301+
if (isset($this->attributes[$key])) {
302+
return $this->attributes[$key];
303+
}
304+
305+
return $defaultValue;
306+
}
307+
291308
/**
292309
* Returns a new instance with the given attribute.
293310
*

0 commit comments

Comments
 (0)