Skip to content

Commit 7f82fd7

Browse files
soyukaabluchet
authored and
abluchet
committed
getItemFromIri now takes an optional context as third argument
1 parent 6cddcd2 commit 7f82fd7

22 files changed

+313
-45
lines changed

src/Api/IriConverterInterface.php

+2-2
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,13 @@ interface IriConverterInterface
2525
* Retrieves an item from its IRI.
2626
*
2727
* @param string $iri
28-
* @param bool $fetchData
28+
* @param array $context
2929
*
3030
* @throws InvalidArgumentException
3131
*
3232
* @return object
3333
*/
34-
public function getItemFromIri(string $iri, bool $fetchData = false);
34+
public function getItemFromIri(string $iri, array $context = []);
3535

3636
/**
3737
* Gets the IRI associated with the given item.

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

+82-9
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,10 @@
1212
namespace ApiPlatform\Core\Bridge\Doctrine\Orm\Extension;
1313

1414
use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryNameGeneratorInterface;
15+
use ApiPlatform\Core\Exception\RuntimeException;
1516
use ApiPlatform\Core\Metadata\Property\Factory\PropertyMetadataFactoryInterface;
1617
use ApiPlatform\Core\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface;
18+
use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface;
1719
use Doctrine\ORM\Mapping\ClassMetadataInfo;
1820
use Doctrine\ORM\QueryBuilder;
1921

@@ -28,35 +30,94 @@ final class EagerLoadingExtension implements QueryCollectionExtensionInterface,
2830
{
2931
private $propertyNameCollectionFactory;
3032
private $propertyMetadataFactory;
33+
private $resourceMetadataFactory;
34+
private $enabled;
35+
private $maxJoins;
36+
private $eagerOnly;
3137

32-
public function __construct(PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory)
38+
public function __construct(PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, ResourceMetadataFactoryInterface $resourceMetadataFactory, bool $enabled = true, int $maxJoins = 30, bool $eagerOnly = true)
3339
{
3440
$this->propertyMetadataFactory = $propertyMetadataFactory;
3541
$this->propertyNameCollectionFactory = $propertyNameCollectionFactory;
42+
$this->resourceMetadataFactory = $resourceMetadataFactory;
43+
$this->enabled = $enabled;
44+
$this->maxJoins = $maxJoins;
45+
$this->eagerOnly = $eagerOnly;
46+
}
47+
48+
/**
49+
* Gets serializer groups once if available, if not it returns the $options array.
50+
*
51+
* @param array $options represents the operation name so that groups are the one of the specific operation
52+
* @param string $resourceClass
53+
* @param string $context normalization_context or denormalization_context
54+
*
55+
* @return string[]
56+
*/
57+
private function getSerializerGroups(string $resourceClass, array $options, string $context): array
58+
{
59+
$resourceMetadata = $this->resourceMetadataFactory->create($resourceClass);
60+
61+
if (isset($options['collection_operation_name'])) {
62+
$context = $resourceMetadata->getCollectionOperationAttribute($options['collection_operation_name'], $context, null, true);
63+
} elseif (isset($options['item_operation_name'])) {
64+
$context = $resourceMetadata->getItemOperationAttribute($options['item_operation_name'], $context, null, true);
65+
} else {
66+
$context = $resourceMetadata->getAttribute($context);
67+
}
68+
69+
if (empty($context['groups'])) {
70+
return $options;
71+
}
72+
73+
return ['serializer_groups' => $context['groups']];
3674
}
3775

3876
/**
3977
* {@inheritdoc}
4078
*/
4179
public function applyToCollection(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, string $operationName = null)
4280
{
81+
if (false === $this->enabled) {
82+
return;
83+
}
84+
85+
$options = [];
86+
4387
if (null !== $operationName) {
44-
$propertyMetadataOptions = ['collection_operation_name' => $operationName];
88+
$options = ['collection_operation_name' => $operationName];
4589
}
4690

47-
$this->joinRelations($queryBuilder, $resourceClass, $propertyMetadataOptions ?? []);
91+
$groups = $this->getSerializerGroups($resourceClass, $options, 'normalization_context');
92+
93+
$this->joinRelations($queryBuilder, $resourceClass, $groups);
4894
}
4995

5096
/**
5197
* {@inheritdoc}
98+
* The context may contain serialization groups which helps defining joined entities that are readable.
5299
*/
53-
public function applyToItem(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, array $identifiers, string $operationName = null)
100+
public function applyToItem(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, array $identifiers, string $operationName = null, array $context = [])
54101
{
102+
if (false === $this->enabled) {
103+
return;
104+
}
105+
106+
$options = [];
107+
55108
if (null !== $operationName) {
56-
$propertyMetadataOptions = ['item_operation_name' => $operationName];
109+
$options = ['item_operation_name' => $operationName];
110+
}
111+
112+
if (isset($context['groups'])) {
113+
$groups = ['serializer_groups' => $context['groups']];
114+
} elseif (isset($context['resource_class'])) {
115+
$groups = $this->getSerializerGroups($context['resource_class'], $options, isset($context['api_denormalize']) ? 'denormalization_context' : 'normalization_context');
116+
} else {
117+
$groups = $this->getSerializerGroups($resourceClass, $options, 'normalization_context');
57118
}
58119

59-
$this->joinRelations($queryBuilder, $resourceClass, $propertyMetadataOptions ?? []);
120+
$this->joinRelations($queryBuilder, $resourceClass, $groups);
60121
}
61122

62123
/**
@@ -68,9 +129,16 @@ public function applyToItem(QueryBuilder $queryBuilder, QueryNameGeneratorInterf
68129
* @param string $originAlias the current entity alias (first o, then a1, a2 etc.)
69130
* @param string $relationAlias the previous relation alias to keep it unique
70131
* @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
132+
* @param int $joinCount the number of joins
133+
*
134+
* @throws RuntimeException when the max number of joins has been reached
71135
*/
72-
private function joinRelations(QueryBuilder $queryBuilder, string $resourceClass, array $propertyMetadataOptions = [], string $originAlias = 'o', string &$relationAlias = 'a', bool $wasLeftJoin = false)
136+
private function joinRelations(QueryBuilder $queryBuilder, string $resourceClass, array $propertyMetadataOptions = [], string $originAlias = 'o', string &$relationAlias = 'a', bool $wasLeftJoin = false, int &$joinCount = 0)
73137
{
138+
if ($joinCount > $this->maxJoins) {
139+
throw new RuntimeException('The total number of joined relations has exceeded the specified maximum. Raise the limit if necessary.');
140+
}
141+
74142
$entityManager = $queryBuilder->getEntityManager();
75143
$classMetadata = $entityManager->getClassMetadata($resourceClass);
76144
$j = 0;
@@ -79,7 +147,11 @@ private function joinRelations(QueryBuilder $queryBuilder, string $resourceClass
79147
foreach ($classMetadata->associationMappings as $association => $mapping) {
80148
$propertyMetadata = $this->propertyMetadataFactory->create($resourceClass, $association, $propertyMetadataOptions);
81149

82-
if (ClassMetadataInfo::FETCH_EAGER !== $mapping['fetch'] || false === $propertyMetadata->isReadableLink()) {
150+
if (true === $this->eagerOnly && ClassMetadataInfo::FETCH_EAGER !== $mapping['fetch']) {
151+
continue;
152+
}
153+
154+
if (false === $propertyMetadata->isReadableLink() || false === $propertyMetadata->isReadable()) {
83155
continue;
84156
}
85157

@@ -97,6 +169,7 @@ private function joinRelations(QueryBuilder $queryBuilder, string $resourceClass
97169

98170
$associationAlias = $relationAlias.$i++;
99171
$queryBuilder->{$method}($originAlias.'.'.$association, $associationAlias);
172+
++$joinCount;
100173
$select = [];
101174
$targetClassMetadata = $entityManager->getClassMetadata($mapping['targetEntity']);
102175

@@ -118,7 +191,7 @@ private function joinRelations(QueryBuilder $queryBuilder, string $resourceClass
118191

119192
$relationAlias .= ++$j;
120193

121-
$this->joinRelations($queryBuilder, $mapping['targetEntity'], $propertyMetadataOptions, $associationAlias, $relationAlias, $method === 'leftJoin');
194+
$this->joinRelations($queryBuilder, $mapping['targetEntity'], $propertyMetadataOptions, $associationAlias, $relationAlias, $method === 'leftJoin', $joinCount);
122195
}
123196
}
124197
}

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

+2-1
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ interface QueryItemExtensionInterface
2828
* @param string $resourceClass
2929
* @param array $identifiers
3030
* @param string|null $operationName
31+
* @param array $context
3132
*/
32-
public function applyToItem(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, array $identifiers, string $operationName = null);
33+
public function applyToItem(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, array $identifiers, string $operationName = null, array $context = []);
3334
}

src/Bridge/Doctrine/Orm/ItemDataProvider.php

+5-2
Original file line numberDiff line numberDiff line change
@@ -55,9 +55,11 @@ public function __construct(ManagerRegistry $managerRegistry, PropertyNameCollec
5555
/**
5656
* {@inheritdoc}
5757
*
58+
* The context may contain a `fetch_data` key representing whether the value should be fetched by Doctrine or if we should return a reference.
59+
*
5860
* @throws RuntimeException
5961
*/
60-
public function getItem(string $resourceClass, $id, string $operationName = null, bool $fetchData = false)
62+
public function getItem(string $resourceClass, $id, string $operationName = null, array $context = [])
6163
{
6264
$manager = $this->managerRegistry->getManagerForClass($resourceClass);
6365
if (null === $manager) {
@@ -66,6 +68,7 @@ public function getItem(string $resourceClass, $id, string $operationName = null
6668

6769
$identifiers = $this->normalizeIdentifiers($id, $manager, $resourceClass);
6870

71+
$fetchData = $context['fetch_data'] ?? false;
6972
if (!$fetchData && $manager instanceof EntityManagerInterface) {
7073
return $manager->getReference($resourceClass, $identifiers);
7174
}
@@ -81,7 +84,7 @@ public function getItem(string $resourceClass, $id, string $operationName = null
8184
$this->addWhereForIdentifiers($identifiers, $queryBuilder);
8285

8386
foreach ($this->itemExtensions as $extension) {
84-
$extension->applyToItem($queryBuilder, $queryNameGenerator, $resourceClass, $identifiers, $operationName);
87+
$extension->applyToItem($queryBuilder, $queryNameGenerator, $resourceClass, $identifiers, $operationName, $context);
8588

8689
if ($extension instanceof QueryResultItemExtensionInterface && $extension->supportsResult($resourceClass, $operationName)) {
8790
return $extension->getResult($queryBuilder);

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

+3
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,9 @@ private function handleConfig(ContainerBuilder $container, array $config, array
9090
$container->setParameter('api_platform.exception_to_status', $config['exception_to_status']);
9191
$container->setParameter('api_platform.formats', $formats);
9292
$container->setParameter('api_platform.error_formats', $errorFormats);
93+
$container->setParameter('api_platform.eager_loading.enabled', $config['eager_loading']['enabled']);
94+
$container->setParameter('api_platform.eager_loading.max_joins', $config['eager_loading']['max_joins']);
95+
$container->setParameter('api_platform.eager_loading.eager_only', $config['eager_loading']['eager_only']);
9396
$container->setParameter('api_platform.collection.order', $config['collection']['order']);
9497
$container->setParameter('api_platform.collection.order_parameter_name', $config['collection']['order_parameter_name']);
9598
$container->setParameter('api_platform.collection.pagination.enabled', $config['collection']['pagination']['enabled']);

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

+9
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,15 @@ public function getConfigTreeBuilder()
4242
->scalarNode('version')->defaultValue('0.0.0')->info('The version of the API.')->end()
4343
->scalarNode('default_operation_path_resolver')->defaultValue('api_platform.operation_path_resolver.underscore')->info('Specify the default operation path resolver to use for generating resources operations path.')->end()
4444
->scalarNode('name_converter')->defaultNull()->info('Specify a name converter to use.')->end()
45+
->arrayNode('eager_loading')
46+
->canBeDisabled()
47+
->addDefaultsIfNotSet()
48+
->children()
49+
->booleanNode('enabled')->defaultTrue()->info('To enable or disable eager loading')->end()
50+
->integerNode('max_joins')->defaultValue(30)->info('Max number of joined relations before EagerLoading throws a RuntimeException')->end()
51+
->booleanNode('eager_only')->defaultTrue()->info('Only eager load relations having an EAGER fetch mode')->end()
52+
->end()
53+
->end()
4554
->booleanNode('enable_fos_user')->defaultValue(false)->info('Enable the FOSUserBundle integration.')->end()
4655
->booleanNode('enable_nelmio_api_doc')->defaultValue(false)->info('Enable the Nelmio Api doc integration.')->end()
4756
->booleanNode('enable_swagger')->defaultValue(true)->info('Enable the Swagger documentation and export.')->end()

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

+4
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,10 @@
9090
<service id="api_platform.doctrine.orm.query_extension.eager_loading" class="ApiPlatform\Core\Bridge\Doctrine\Orm\Extension\EagerLoadingExtension" public="false">
9191
<argument type="service" id="api_platform.metadata.property.name_collection_factory" />
9292
<argument type="service" id="api_platform.metadata.property.metadata_factory" />
93+
<argument type="service" id="api_platform.metadata.resource.metadata_factory" />
94+
<argument>%api_platform.eager_loading.enabled%</argument>
95+
<argument>%api_platform.eager_loading.max_joins%</argument>
96+
<argument>%api_platform.eager_loading.eager_only%</argument>
9397

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

src/Bridge/Symfony/Routing/IriConverter.php

+2-2
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ public function __construct(PropertyNameCollectionFactoryInterface $propertyName
5353
/**
5454
* {@inheritdoc}
5555
*/
56-
public function getItemFromIri(string $iri, bool $fetchData = false)
56+
public function getItemFromIri(string $iri, array $context = [])
5757
{
5858
try {
5959
$parameters = $this->router->match($iri);
@@ -65,7 +65,7 @@ public function getItemFromIri(string $iri, bool $fetchData = false)
6565
throw new InvalidArgumentException(sprintf('No resource associated to "%s".', $iri));
6666
}
6767

68-
if ($item = $this->itemDataProvider->getItem($parameters['_api_resource_class'], $parameters['id'], null, $fetchData)) {
68+
if ($item = $this->itemDataProvider->getItem($parameters['_api_resource_class'], $parameters['id'], null, $context)) {
6969
return $item;
7070
}
7171

src/DataProvider/ChainItemDataProvider.php

+2-2
Original file line numberDiff line numberDiff line change
@@ -33,11 +33,11 @@ public function __construct(array $dataProviders)
3333
/**
3434
* {@inheritdoc}
3535
*/
36-
public function getItem(string $resourceClass, $id, string $operationName = null, bool $fetchData = false)
36+
public function getItem(string $resourceClass, $id, string $operationName = null, array $context = [])
3737
{
3838
foreach ($this->dataProviders as $dataProviders) {
3939
try {
40-
return $dataProviders->getItem($resourceClass, $id, $operationName, $fetchData);
40+
return $dataProviders->getItem($resourceClass, $id, $operationName, $context);
4141
} catch (ResourceClassNotSupportedException $e) {
4242
continue;
4343
}

src/DataProvider/ItemDataProviderInterface.php

+2-2
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,11 @@ interface ItemDataProviderInterface
2626
* @param string $resourceClass
2727
* @param string|null $operationName
2828
* @param int|string $id
29-
* @param bool $fetchData
29+
* @param array $context
3030
*
3131
* @throws ResourceClassNotSupportedException
3232
*
3333
* @return object|null
3434
*/
35-
public function getItem(string $resourceClass, $id, string $operationName = null, bool $fetchData = false);
35+
public function getItem(string $resourceClass, $id, string $operationName = null, array $context = []);
3636
}

src/EventListener/ReadListener.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ private function getCollectionData(Request $request, array $attributes)
9191
private function getItemData(Request $request, array $attributes)
9292
{
9393
$id = $request->attributes->get('id');
94-
$data = $this->itemDataProvider->getItem($attributes['resource_class'], $id, $attributes['item_operation_name'], true);
94+
$data = $this->itemDataProvider->getItem($attributes['resource_class'], $id, $attributes['item_operation_name'], ['fetch_data' => true]);
9595

9696
if (null === $data) {
9797
throw new NotFoundHttpException('Not Found');

src/JsonLd/Serializer/ItemNormalizer.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ public function denormalize($data, $class, $format = null, array $context = [])
9595
throw new InvalidArgumentException('Update is not allowed for this operation.');
9696
}
9797

98-
$context['object_to_populate'] = $this->iriConverter->getItemFromIri($data['@id'], true);
98+
$context['object_to_populate'] = $this->iriConverter->getItemFromIri($data['@id'], ['fetch_data' => true] + $context);
9999
}
100100

101101
return parent::denormalize($data, $class, $format, $context);

src/Serializer/AbstractItemNormalizer.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -272,7 +272,7 @@ private function denormalizeRelation(string $attributeName, PropertyMetadata $pr
272272
{
273273
if (is_string($value)) {
274274
try {
275-
return $this->iriConverter->getItemFromIri($value, true);
275+
return $this->iriConverter->getItemFromIri($value, ['fetch_data' => true] + $context);
276276
} catch (InvalidArgumentException $e) {
277277
// Give a chance to other normalizers (e.g.: DateTimeNormalizer)
278278
}

src/Serializer/ItemNormalizer.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ public function denormalize($data, $class, $format = null, array $context = [])
3333
throw new InvalidArgumentException('Update is not allowed for this operation.');
3434
}
3535

36-
$context['object_to_populate'] = $this->iriConverter->getItemFromIri($data['id'], true);
36+
$context['object_to_populate'] = $this->iriConverter->getItemFromIri($data['id'], ['fetch_data' => true] + $context);
3737
}
3838

3939
return parent::denormalize($data, $class, $format, $context);

0 commit comments

Comments
 (0)