Skip to content

Commit d78909b

Browse files
committed
Merge branch '2.0'
2 parents 29c52ca + a6307b2 commit d78909b

File tree

18 files changed

+212
-36
lines changed

18 files changed

+212
-36
lines changed

.php_cs.dist

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,14 @@
11
<?php
22

3+
$header = <<<'HEADER'
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+
HEADER;
11+
312
$finder = PhpCsFixer\Finder::create()
413
->in(__DIR__)
514
->exclude('tests/Fixtures/app/cache')
@@ -17,6 +26,10 @@ return PhpCsFixer\Config::create()
1726
'allow_single_line_closure' => true,
1827
],
1928
'declare_strict_types' => true,
29+
'header_comment' => [
30+
'header' => $header,
31+
'location' => 'after_open',
32+
],
2033
'modernize_types_casting' => true,
2134
// 'native_function_invocation' => true,
2235
'no_extra_consecutive_blank_lines' => [

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/Filter/AbstractFilter.php

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
namespace ApiPlatform\Core\Bridge\Doctrine\Orm\Filter;
1515

16+
use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryChecker;
1617
use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryNameGeneratorInterface;
1718
use ApiPlatform\Core\Exception\InvalidArgumentException;
1819
use ApiPlatform\Core\Util\RequestParser;
@@ -287,8 +288,14 @@ protected function addJoinOnce(QueryBuilder $queryBuilder, QueryNameGeneratorInt
287288

288289
if (null === $join) {
289290
$associationAlias = $queryNameGenerator->generateJoinAlias($association);
290-
$queryBuilder
291-
->join(sprintf('%s.%s', $alias, $association), $associationAlias);
291+
292+
if (true === QueryChecker::hasLeftJoin($queryBuilder)) {
293+
$queryBuilder
294+
->leftJoin(sprintf('%s.%s', $alias, $association), $associationAlias);
295+
} else {
296+
$queryBuilder
297+
->innerJoin(sprintf('%s.%s', $alias, $association), $associationAlias);
298+
}
292299
} else {
293300
$associationAlias = $join->getAlias();
294301
}

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/Doctrine/Orm/Util/QueryChecker.php

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515

1616
use Doctrine\Common\Persistence\ManagerRegistry;
1717
use Doctrine\ORM\Mapping\ClassMetadata;
18+
use Doctrine\ORM\Query\Expr\Join;
1819
use Doctrine\ORM\QueryBuilder;
1920

2021
/**
@@ -155,4 +156,24 @@ public static function hasOrderByOnToManyJoin(QueryBuilder $queryBuilder, Manage
155156

156157
return false;
157158
}
159+
160+
/**
161+
* Determines whether the query builder already has a left join.
162+
*
163+
* @param QueryBuilder $queryBuilder
164+
*
165+
* @return bool
166+
*/
167+
public static function hasLeftJoin(QueryBuilder $queryBuilder): bool
168+
{
169+
foreach ($queryBuilder->getDQLPart('join') as $dqlParts) {
170+
foreach ($dqlParts as $dqlPart) {
171+
if (Join::LEFT_JOIN === $dqlPart->getJoinType()) {
172+
return true;
173+
}
174+
}
175+
}
176+
177+
return false;
178+
}
158179
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@ private function handleConfig(ContainerBuilder $container, array $config, array
117117
$container->setParameter('api_platform.api_resources_directory', $config['api_resources_directory']);
118118
$container->setParameter('api_platform.eager_loading.enabled', $config['eager_loading']['enabled']);
119119
$container->setParameter('api_platform.eager_loading.max_joins', $config['eager_loading']['max_joins']);
120+
$container->setParameter('api_platform.eager_loading.fetch_partial', $config['eager_loading']['fetch_partial']);
120121
$container->setParameter('api_platform.eager_loading.force_eager', $config['eager_loading']['force_eager']);
121122
$container->setParameter('api_platform.collection.order', $config['collection']['order']);
122123
$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
@@ -51,6 +51,7 @@ public function getConfigTreeBuilder()
5151
->addDefaultsIfNotSet()
5252
->children()
5353
->booleanNode('enabled')->defaultTrue()->info('To enable or disable eager loading')->end()
54+
->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()
5455
->integerNode('max_joins')->defaultValue(30)->info('Max number of joined relations before EagerLoading throws a RuntimeException')->end()
5556
->booleanNode('force_eager')->defaultTrue()->info('Force join on every relation. If disabled, it will only join relations having the EAGER fetch mode.')->end()
5657
->end()

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,7 @@
113113
<argument>%api_platform.eager_loading.force_eager%</argument>
114114
<argument type="service" id="request_stack" />
115115
<argument type="service" id="api_platform.serializer.context_builder" />
116+
<argument>%api_platform.eager_loading.fetch_partial%</argument>
116117

117118
<tag name="api_platform.doctrine.orm.query_extension.item" priority="64" />
118119
<tag name="api_platform.doctrine.orm.query_extension.collection" priority="64" />

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

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -64,11 +64,6 @@
6464
<argument type="service" id="api_platform.metadata.property.metadata_factory.serializer.inner" />
6565
</service>
6666

67-
<service id="api_platform.metadata.property.metadata_factory.validator" class="ApiPlatform\Core\Bridge\Symfony\Validator\Metadata\Property\ValidatorPropertyMetadataFactory" decorates="api_platform.metadata.property.metadata_factory" decoration-priority="20" public="false">
68-
<argument type="service" id="validator" />
69-
<argument type="service" id="api_platform.metadata.property.metadata_factory.validator.inner" />
70-
</service>
71-
7267
<service id="api_platform.metadata.property.metadata_factory.cached" class="ApiPlatform\Core\Metadata\Property\Factory\CachedPropertyMetadataFactory" decorates="api_platform.metadata.property.metadata_factory" decoration-priority="-10" public="false">
7368
<argument type="service" id="api_platform.cache.metadata.property" />
7469
<argument type="service" id="api_platform.metadata.property.metadata_factory.cached.inner" />

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,11 @@
55
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
66

77
<services>
8+
<service id="api_platform.metadata.property.metadata_factory.validator" class="ApiPlatform\Core\Bridge\Symfony\Validator\Metadata\Property\ValidatorPropertyMetadataFactory" decorates="api_platform.metadata.property.metadata_factory" decoration-priority="20" public="false">
9+
<argument type="service" id="validator" />
10+
<argument type="service" id="api_platform.metadata.property.metadata_factory.validator.inner" />
11+
</service>
12+
813
<service id="api_platform.listener.view.validate" class="ApiPlatform\Core\Bridge\Symfony\Validator\EventListener\ValidateListener">
914
<argument type="service" id="validator" />
1015
<argument type="service" id="api_platform.metadata.resource.metadata_factory" />

src/Metadata/Property/PropertyMetadata.php

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

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

src/Serializer/AbstractItemNormalizer.php

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -133,8 +133,9 @@ protected function getAllowedAttributes($classOrObject, array $context, $attribu
133133
$propertyMetadata = $this->propertyMetadataFactory->create($context['resource_class'], $propertyName, $options);
134134

135135
if (
136-
(isset($context['api_normalize']) && $propertyMetadata->isReadable()) ||
137-
(isset($context['api_denormalize']) && $propertyMetadata->isWritable())
136+
$this->isAllowedAttribute($classOrObject, $propertyName, null, $context) &&
137+
((isset($context['api_normalize']) && $propertyMetadata->isReadable()) ||
138+
(isset($context['api_denormalize']) && $propertyMetadata->isWritable()))
138139
) {
139140
$allowedAttributes[] = $propertyName;
140141
}

0 commit comments

Comments
 (0)