Skip to content

Commit

Permalink
getItemFromIri now takes an optional context as third argument
Browse files Browse the repository at this point in the history
  • Loading branch information
soyuka authored and abluchet committed Nov 3, 2016
1 parent 6cddcd2 commit 7f82fd7
Show file tree
Hide file tree
Showing 22 changed files with 313 additions and 45 deletions.
4 changes: 2 additions & 2 deletions src/Api/IriConverterInterface.php
Expand Up @@ -25,13 +25,13 @@ interface IriConverterInterface
* Retrieves an item from its IRI.
*
* @param string $iri
* @param bool $fetchData
* @param array $context
*
* @throws InvalidArgumentException
*
* @return object
*/
public function getItemFromIri(string $iri, bool $fetchData = false);
public function getItemFromIri(string $iri, array $context = []);

/**
* Gets the IRI associated with the given item.
Expand Down
91 changes: 82 additions & 9 deletions src/Bridge/Doctrine/Orm/Extension/EagerLoadingExtension.php
Expand Up @@ -12,8 +12,10 @@
namespace ApiPlatform\Core\Bridge\Doctrine\Orm\Extension;

use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryNameGeneratorInterface;
use ApiPlatform\Core\Exception\RuntimeException;
use ApiPlatform\Core\Metadata\Property\Factory\PropertyMetadataFactoryInterface;
use ApiPlatform\Core\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface;
use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface;
use Doctrine\ORM\Mapping\ClassMetadataInfo;
use Doctrine\ORM\QueryBuilder;

Expand All @@ -28,35 +30,94 @@ final class EagerLoadingExtension implements QueryCollectionExtensionInterface,
{
private $propertyNameCollectionFactory;
private $propertyMetadataFactory;
private $resourceMetadataFactory;
private $enabled;
private $maxJoins;
private $eagerOnly;

public function __construct(PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory)
public function __construct(PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, ResourceMetadataFactoryInterface $resourceMetadataFactory, bool $enabled = true, int $maxJoins = 30, bool $eagerOnly = true)
{
$this->propertyMetadataFactory = $propertyMetadataFactory;
$this->propertyNameCollectionFactory = $propertyNameCollectionFactory;
$this->resourceMetadataFactory = $resourceMetadataFactory;
$this->enabled = $enabled;
$this->maxJoins = $maxJoins;
$this->eagerOnly = $eagerOnly;
}

/**
* Gets serializer groups once if available, if not it returns the $options array.
*
* @param array $options represents the operation name so that groups are the one of the specific operation
* @param string $resourceClass
* @param string $context normalization_context or denormalization_context
*
* @return string[]
*/
private function getSerializerGroups(string $resourceClass, array $options, string $context): array
{
$resourceMetadata = $this->resourceMetadataFactory->create($resourceClass);

if (isset($options['collection_operation_name'])) {
$context = $resourceMetadata->getCollectionOperationAttribute($options['collection_operation_name'], $context, null, true);
} elseif (isset($options['item_operation_name'])) {
$context = $resourceMetadata->getItemOperationAttribute($options['item_operation_name'], $context, null, true);
} else {
$context = $resourceMetadata->getAttribute($context);
}

if (empty($context['groups'])) {
return $options;
}

return ['serializer_groups' => $context['groups']];
}

/**
* {@inheritdoc}
*/
public function applyToCollection(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, string $operationName = null)
{
if (false === $this->enabled) {
return;
}

$options = [];

if (null !== $operationName) {
$propertyMetadataOptions = ['collection_operation_name' => $operationName];
$options = ['collection_operation_name' => $operationName];
}

$this->joinRelations($queryBuilder, $resourceClass, $propertyMetadataOptions ?? []);
$groups = $this->getSerializerGroups($resourceClass, $options, 'normalization_context');

$this->joinRelations($queryBuilder, $resourceClass, $groups);
}

/**
* {@inheritdoc}
* The context may contain serialization groups which helps defining joined entities that are readable.
*/
public function applyToItem(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, array $identifiers, string $operationName = null)
public function applyToItem(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, array $identifiers, string $operationName = null, array $context = [])
{
if (false === $this->enabled) {
return;
}

$options = [];

if (null !== $operationName) {
$propertyMetadataOptions = ['item_operation_name' => $operationName];
$options = ['item_operation_name' => $operationName];
}

if (isset($context['groups'])) {
$groups = ['serializer_groups' => $context['groups']];
} elseif (isset($context['resource_class'])) {
$groups = $this->getSerializerGroups($context['resource_class'], $options, isset($context['api_denormalize']) ? 'denormalization_context' : 'normalization_context');
} else {
$groups = $this->getSerializerGroups($resourceClass, $options, 'normalization_context');
}

$this->joinRelations($queryBuilder, $resourceClass, $propertyMetadataOptions ?? []);
$this->joinRelations($queryBuilder, $resourceClass, $groups);
}

/**
Expand All @@ -68,9 +129,16 @@ public function applyToItem(QueryBuilder $queryBuilder, QueryNameGeneratorInterf
* @param string $originAlias the current entity alias (first o, then a1, a2 etc.)
* @param string $relationAlias the previous relation alias to keep it unique
* @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
* @param int $joinCount the number of joins
*
* @throws RuntimeException when the max number of joins has been reached
*/
private function joinRelations(QueryBuilder $queryBuilder, string $resourceClass, array $propertyMetadataOptions = [], string $originAlias = 'o', string &$relationAlias = 'a', bool $wasLeftJoin = false)
private function joinRelations(QueryBuilder $queryBuilder, string $resourceClass, array $propertyMetadataOptions = [], string $originAlias = 'o', string &$relationAlias = 'a', bool $wasLeftJoin = false, int &$joinCount = 0)
{
if ($joinCount > $this->maxJoins) {
throw new RuntimeException('The total number of joined relations has exceeded the specified maximum. Raise the limit if necessary.');
}

$entityManager = $queryBuilder->getEntityManager();
$classMetadata = $entityManager->getClassMetadata($resourceClass);
$j = 0;
Expand All @@ -79,7 +147,11 @@ private function joinRelations(QueryBuilder $queryBuilder, string $resourceClass
foreach ($classMetadata->associationMappings as $association => $mapping) {
$propertyMetadata = $this->propertyMetadataFactory->create($resourceClass, $association, $propertyMetadataOptions);

if (ClassMetadataInfo::FETCH_EAGER !== $mapping['fetch'] || false === $propertyMetadata->isReadableLink()) {
if (true === $this->eagerOnly && ClassMetadataInfo::FETCH_EAGER !== $mapping['fetch']) {
continue;
}

if (false === $propertyMetadata->isReadableLink() || false === $propertyMetadata->isReadable()) {
continue;
}

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

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

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

$relationAlias .= ++$j;

$this->joinRelations($queryBuilder, $mapping['targetEntity'], $propertyMetadataOptions, $associationAlias, $relationAlias, $method === 'leftJoin');
$this->joinRelations($queryBuilder, $mapping['targetEntity'], $propertyMetadataOptions, $associationAlias, $relationAlias, $method === 'leftJoin', $joinCount);
}
}
}
Expand Up @@ -28,6 +28,7 @@ interface QueryItemExtensionInterface
* @param string $resourceClass
* @param array $identifiers
* @param string|null $operationName
* @param array $context
*/
public function applyToItem(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, array $identifiers, string $operationName = null);
public function applyToItem(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, array $identifiers, string $operationName = null, array $context = []);
}
7 changes: 5 additions & 2 deletions src/Bridge/Doctrine/Orm/ItemDataProvider.php
Expand Up @@ -55,9 +55,11 @@ public function __construct(ManagerRegistry $managerRegistry, PropertyNameCollec
/**
* {@inheritdoc}
*
* The context may contain a `fetch_data` key representing whether the value should be fetched by Doctrine or if we should return a reference.
*
* @throws RuntimeException
*/
public function getItem(string $resourceClass, $id, string $operationName = null, bool $fetchData = false)
public function getItem(string $resourceClass, $id, string $operationName = null, array $context = [])
{
$manager = $this->managerRegistry->getManagerForClass($resourceClass);
if (null === $manager) {
Expand All @@ -66,6 +68,7 @@ public function getItem(string $resourceClass, $id, string $operationName = null

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

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

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

if ($extension instanceof QueryResultItemExtensionInterface && $extension->supportsResult($resourceClass, $operationName)) {
return $extension->getResult($queryBuilder);
Expand Down
Expand Up @@ -90,6 +90,9 @@ private function handleConfig(ContainerBuilder $container, array $config, array
$container->setParameter('api_platform.exception_to_status', $config['exception_to_status']);
$container->setParameter('api_platform.formats', $formats);
$container->setParameter('api_platform.error_formats', $errorFormats);
$container->setParameter('api_platform.eager_loading.enabled', $config['eager_loading']['enabled']);
$container->setParameter('api_platform.eager_loading.max_joins', $config['eager_loading']['max_joins']);
$container->setParameter('api_platform.eager_loading.eager_only', $config['eager_loading']['eager_only']);
$container->setParameter('api_platform.collection.order', $config['collection']['order']);
$container->setParameter('api_platform.collection.order_parameter_name', $config['collection']['order_parameter_name']);
$container->setParameter('api_platform.collection.pagination.enabled', $config['collection']['pagination']['enabled']);
Expand Down
Expand Up @@ -42,6 +42,15 @@ public function getConfigTreeBuilder()
->scalarNode('version')->defaultValue('0.0.0')->info('The version of the API.')->end()
->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()
->scalarNode('name_converter')->defaultNull()->info('Specify a name converter to use.')->end()
->arrayNode('eager_loading')
->canBeDisabled()
->addDefaultsIfNotSet()
->children()
->booleanNode('enabled')->defaultTrue()->info('To enable or disable eager loading')->end()
->integerNode('max_joins')->defaultValue(30)->info('Max number of joined relations before EagerLoading throws a RuntimeException')->end()
->booleanNode('eager_only')->defaultTrue()->info('Only eager load relations having an EAGER fetch mode')->end()
->end()
->end()
->booleanNode('enable_fos_user')->defaultValue(false)->info('Enable the FOSUserBundle integration.')->end()
->booleanNode('enable_nelmio_api_doc')->defaultValue(false)->info('Enable the Nelmio Api doc integration.')->end()
->booleanNode('enable_swagger')->defaultValue(true)->info('Enable the Swagger documentation and export.')->end()
Expand Down
4 changes: 4 additions & 0 deletions src/Bridge/Symfony/Bundle/Resources/config/doctrine_orm.xml
Expand Up @@ -90,6 +90,10 @@
<service id="api_platform.doctrine.orm.query_extension.eager_loading" class="ApiPlatform\Core\Bridge\Doctrine\Orm\Extension\EagerLoadingExtension" public="false">
<argument type="service" id="api_platform.metadata.property.name_collection_factory" />
<argument type="service" id="api_platform.metadata.property.metadata_factory" />
<argument type="service" id="api_platform.metadata.resource.metadata_factory" />
<argument>%api_platform.eager_loading.enabled%</argument>
<argument>%api_platform.eager_loading.max_joins%</argument>
<argument>%api_platform.eager_loading.eager_only%</argument>

<tag name="api_platform.doctrine.orm.query_extension.item" priority="64" />
<tag name="api_platform.doctrine.orm.query_extension.collection" priority="64" />
Expand Down
4 changes: 2 additions & 2 deletions src/Bridge/Symfony/Routing/IriConverter.php
Expand Up @@ -53,7 +53,7 @@ public function __construct(PropertyNameCollectionFactoryInterface $propertyName
/**
* {@inheritdoc}
*/
public function getItemFromIri(string $iri, bool $fetchData = false)
public function getItemFromIri(string $iri, array $context = [])
{
try {
$parameters = $this->router->match($iri);
Expand All @@ -65,7 +65,7 @@ public function getItemFromIri(string $iri, bool $fetchData = false)
throw new InvalidArgumentException(sprintf('No resource associated to "%s".', $iri));
}

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

Expand Down
4 changes: 2 additions & 2 deletions src/DataProvider/ChainItemDataProvider.php
Expand Up @@ -33,11 +33,11 @@ public function __construct(array $dataProviders)
/**
* {@inheritdoc}
*/
public function getItem(string $resourceClass, $id, string $operationName = null, bool $fetchData = false)
public function getItem(string $resourceClass, $id, string $operationName = null, array $context = [])
{
foreach ($this->dataProviders as $dataProviders) {
try {
return $dataProviders->getItem($resourceClass, $id, $operationName, $fetchData);
return $dataProviders->getItem($resourceClass, $id, $operationName, $context);
} catch (ResourceClassNotSupportedException $e) {
continue;
}
Expand Down
4 changes: 2 additions & 2 deletions src/DataProvider/ItemDataProviderInterface.php
Expand Up @@ -26,11 +26,11 @@ interface ItemDataProviderInterface
* @param string $resourceClass
* @param string|null $operationName
* @param int|string $id
* @param bool $fetchData
* @param array $context
*
* @throws ResourceClassNotSupportedException
*
* @return object|null
*/
public function getItem(string $resourceClass, $id, string $operationName = null, bool $fetchData = false);
public function getItem(string $resourceClass, $id, string $operationName = null, array $context = []);
}
2 changes: 1 addition & 1 deletion src/EventListener/ReadListener.php
Expand Up @@ -91,7 +91,7 @@ private function getCollectionData(Request $request, array $attributes)
private function getItemData(Request $request, array $attributes)
{
$id = $request->attributes->get('id');
$data = $this->itemDataProvider->getItem($attributes['resource_class'], $id, $attributes['item_operation_name'], true);
$data = $this->itemDataProvider->getItem($attributes['resource_class'], $id, $attributes['item_operation_name'], ['fetch_data' => true]);

if (null === $data) {
throw new NotFoundHttpException('Not Found');
Expand Down
2 changes: 1 addition & 1 deletion src/JsonLd/Serializer/ItemNormalizer.php
Expand Up @@ -95,7 +95,7 @@ public function denormalize($data, $class, $format = null, array $context = [])
throw new InvalidArgumentException('Update is not allowed for this operation.');
}

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

return parent::denormalize($data, $class, $format, $context);
Expand Down
2 changes: 1 addition & 1 deletion src/Serializer/AbstractItemNormalizer.php
Expand Up @@ -272,7 +272,7 @@ private function denormalizeRelation(string $attributeName, PropertyMetadata $pr
{
if (is_string($value)) {
try {
return $this->iriConverter->getItemFromIri($value, true);
return $this->iriConverter->getItemFromIri($value, ['fetch_data' => true] + $context);
} catch (InvalidArgumentException $e) {
// Give a chance to other normalizers (e.g.: DateTimeNormalizer)
}
Expand Down
2 changes: 1 addition & 1 deletion src/Serializer/ItemNormalizer.php
Expand Up @@ -33,7 +33,7 @@ public function denormalize($data, $class, $format = null, array $context = [])
throw new InvalidArgumentException('Update is not allowed for this operation.');
}

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

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

0 comments on commit 7f82fd7

Please sign in to comment.