Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

GraphQL + data provider refactoring: automatically add SQL join clauses #1619

Merged
merged 5 commits into from
Dec 29, 2017
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
18 changes: 9 additions & 9 deletions src/Bridge/Doctrine/Orm/CollectionDataProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,11 @@

namespace ApiPlatform\Core\Bridge\Doctrine\Orm;

use ApiPlatform\Core\Bridge\Doctrine\Orm\Extension\ContextAwareQueryCollectionExtensionInterface;
use ApiPlatform\Core\Bridge\Doctrine\Orm\Extension\QueryCollectionExtensionInterface;
use ApiPlatform\Core\Bridge\Doctrine\Orm\Extension\QueryResultCollectionExtensionInterface;
use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryNameGenerator;
use ApiPlatform\Core\DataProvider\CollectionDataProviderInterface;
use ApiPlatform\Core\DataProvider\ContextAwareCollectionDataProviderInterface;
use ApiPlatform\Core\DataProvider\RestrictedDataProviderInterface;
use ApiPlatform\Core\Exception\RuntimeException;
use Doctrine\Common\Persistence\ManagerRegistry;
Expand All @@ -27,22 +28,21 @@
* @author Kévin Dunglas <dunglas@gmail.com>
* @author Samuel ROZE <samuel.roze@gmail.com>
*/
class CollectionDataProvider implements CollectionDataProviderInterface, RestrictedDataProviderInterface
class CollectionDataProvider implements ContextAwareCollectionDataProviderInterface, RestrictedDataProviderInterface
{
private $managerRegistry;
private $collectionExtensions;

/**
* @param ManagerRegistry $managerRegistry
* @param QueryCollectionExtensionInterface[] $collectionExtensions
* @param QueryCollectionExtensionInterface[]|ContextAwareQueryCollectionExtensionInterface[] $collectionExtensions
*/
public function __construct(ManagerRegistry $managerRegistry, array $collectionExtensions = [])
{
$this->managerRegistry = $managerRegistry;
$this->collectionExtensions = $collectionExtensions;
}

public function supports(string $resourceClass, string $operationName = null): bool
public function supports(string $resourceClass, string $operationName = null, array $context = []): bool
{
return null !== $this->managerRegistry->getManagerForClass($resourceClass);
}
Expand All @@ -52,7 +52,7 @@ public function supports(string $resourceClass, string $operationName = null): b
*
* @throws RuntimeException
*/
public function getCollection(string $resourceClass, string $operationName = null)
public function getCollection(string $resourceClass, string $operationName = null, array $context = [])
{
$manager = $this->managerRegistry->getManagerForClass($resourceClass);

Expand All @@ -64,10 +64,10 @@ public function getCollection(string $resourceClass, string $operationName = nul
$queryBuilder = $repository->createQueryBuilder('o');
$queryNameGenerator = new QueryNameGenerator();
foreach ($this->collectionExtensions as $extension) {
$extension->applyToCollection($queryBuilder, $queryNameGenerator, $resourceClass, $operationName);
$extension->applyToCollection($queryBuilder, $queryNameGenerator, $resourceClass, $operationName, $context);

if ($extension instanceof QueryResultCollectionExtensionInterface && $extension->supportsResult($resourceClass, $operationName)) {
return $extension->getResult($queryBuilder, $resourceClass, $operationName);
if ($extension instanceof QueryResultCollectionExtensionInterface && $extension->supportsResult($resourceClass, $operationName, $context)) {
return $extension->getResult($queryBuilder, $resourceClass, $operationName, $context);
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?php

/*
* This file is part of the API Platform project.
*
* (c) Kévin Dunglas <dunglas@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

declare(strict_types=1);

namespace ApiPlatform\Core\Bridge\Doctrine\Orm\Extension;

use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryNameGeneratorInterface;
use Doctrine\ORM\QueryBuilder;

/**
* Context aware extension.
*
* @author Kévin Dunglas <dunglas@gmail.com>
*/
interface ContextAwareQueryCollectionExtensionInterface extends QueryCollectionExtensionInterface
{
public function applyToCollection(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, string $operationName = null, array $context = []);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php

/*
* This file is part of the API Platform project.
*
* (c) Kévin Dunglas <dunglas@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

declare(strict_types=1);

namespace ApiPlatform\Core\Bridge\Doctrine\Orm\Extension;

use Doctrine\ORM\QueryBuilder;

/**
* Context aware extension.
*
* @author Kévin Dunglas <dunglas@gmail.com>
*/
interface ContextAwareQueryItemExtensionInterface extends QueryItemExtensionInterface
{
public function supportsResult(string $resourceClass, string $operationName = null, array $context = []): bool;

/**
* @return mixed
*/
public function getResult(QueryBuilder $queryBuilder, string $resourceClass = null, string $operationName = null, array $context = []);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php

/*
* This file is part of the API Platform project.
*
* (c) Kévin Dunglas <dunglas@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

declare(strict_types=1);

namespace ApiPlatform\Core\Bridge\Doctrine\Orm\Extension;

use Doctrine\ORM\QueryBuilder;

/**
* Context aware extension.
*
* @author Kévin Dunglas <dunglas@gmail.com>
*/
interface ContextAwareQueryResultCollectionExtensionInterface extends QueryResultCollectionExtensionInterface
{
public function supportsResult(string $resourceClass, string $operationName = null, array $context = []): bool;

/**
* @return mixed
*/
public function getResult(QueryBuilder $queryBuilder, string $resourceClass = null, string $operationName = null, array $context = []);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php

/*
* This file is part of the API Platform project.
*
* (c) Kévin Dunglas <dunglas@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

declare(strict_types=1);

namespace ApiPlatform\Core\Bridge\Doctrine\Orm\Extension;

use Doctrine\ORM\QueryBuilder;

/**
* Context aware extension.
*
* @author Kévin Dunglas <dunglas@gmail.com>
*/
interface ContextAwareQueryResultItemExtensionInterface extends QueryResultItemExtensionInterface
{
public function supportsResult(string $resourceClass, string $operationName = null, array $context = []): bool;

/**
* @return mixed
*/
public function getResult(QueryBuilder $queryBuilder, string $resourceClass = null, string $operationName = null, array $context = []);
}
89 changes: 43 additions & 46 deletions src/Bridge/Doctrine/Orm/Extension/EagerLoadingExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@

use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\EagerLoadingTrait;
use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryNameGeneratorInterface;
use ApiPlatform\Core\Exception\InvalidArgumentException;
use ApiPlatform\Core\Exception\PropertyNotFoundException;
use ApiPlatform\Core\Exception\ResourceClassNotFoundException;
use ApiPlatform\Core\Exception\RuntimeException;
Expand All @@ -37,7 +38,7 @@
* @author Antoine Bluchet <soyuka@gmail.com>
* @author Baptiste Meyer <baptiste.meyer@gmail.com>
*/
final class EagerLoadingExtension implements QueryCollectionExtensionInterface, QueryItemExtensionInterface
final class EagerLoadingExtension implements ContextAwareQueryCollectionExtensionInterface, QueryItemExtensionInterface
{
use EagerLoadingTrait;

Expand All @@ -53,6 +54,10 @@ final class EagerLoadingExtension implements QueryCollectionExtensionInterface,
*/
public function __construct(PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, ResourceMetadataFactoryInterface $resourceMetadataFactory, int $maxJoins = 30, bool $forceEager = true, RequestStack $requestStack = null, SerializerContextBuilderInterface $serializerContextBuilder = null, bool $fetchPartial = false, ClassMetadataFactoryInterface $classMetadataFactory = null)
{
//if (null !== $this->serializerContextBuilder) {
// @trigger_error('Passing an instance of "%s" is deprecated since version 2.2 and will be removed in 3.0. Use the "normalization_context" of the data provider\'s context instead.', E_USER_DEPRECATED);
//}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You want to keep this dead code?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No I'll drop it, I try to debug something.


$this->propertyNameCollectionFactory = $propertyNameCollectionFactory;
$this->propertyMetadataFactory = $propertyMetadataFactory;
$this->resourceMetadataFactory = $resourceMetadataFactory;
Expand All @@ -67,33 +72,39 @@ public function __construct(PropertyNameCollectionFactoryInterface $propertyName
/**
* {@inheritdoc}
*/
public function applyToCollection(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, string $operationName = null)
public function applyToCollection(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass = null, string $operationName = null, array $context = [])
{
$options = null === $operationName ? [] : ['collection_operation_name' => $operationName];

$forceEager = $this->shouldOperationForceEager($resourceClass, $options);
$fetchPartial = $this->shouldOperationFetchPartial($resourceClass, $options);
$serializerContext = $this->getPropertyMetadataOptions($resourceClass, 'normalization_context', $options);

$groups = $this->getSerializerGroups($options, $serializerContext);

$this->joinRelations($queryBuilder, $queryNameGenerator, $resourceClass, $forceEager, $fetchPartial, $queryBuilder->getRootAliases()[0], $groups, $serializerContext);
$this->apply(true, $queryBuilder, $queryNameGenerator, $resourceClass, $operationName, $context);
}

/**
* 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, array $context = [])
{
$options = null === $operationName ? [] : ['item_operation_name' => $operationName];
$this->apply(false, $queryBuilder, $queryNameGenerator, $resourceClass, $operationName, $context);
}

private function apply(bool $collection, QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass = null, string $operationName = null, array $context)
{
if (null === $resourceClass) {
throw new InvalidArgumentException('The "$resourceClass" parameter must not be null');
}

$options = [];
if (null !== $operationName) {
$options[($collection ? 'collection' : 'item').'_operation_name'] = $operationName;
}

$forceEager = $this->shouldOperationForceEager($resourceClass, $options);
$fetchPartial = $this->shouldOperationFetchPartial($resourceClass, $options);
$contextType = isset($context['api_denormalize']) ? 'denormalization_context' : 'normalization_context';
$propertyMetadataOptions = $this->getPropertyMetadataOptions($context['resource_class'] ?? $resourceClass, $contextType, $options);
$serializerGroups = $this->getSerializerGroups($options, $propertyMetadataOptions);

$this->joinRelations($queryBuilder, $queryNameGenerator, $resourceClass, $forceEager, $fetchPartial, $queryBuilder->getRootAliases()[0], $serializerGroups, $propertyMetadataOptions);
if (!$normalizationContext = $context['normalization_context'] ?? false) {
$contextType = isset($context['api_denormalize']) ? 'denormalization_context' : 'normalization_context';
$normalizationContext = $this->getNormalizationContext($context['resource_class'] ?? $resourceClass, $contextType, $options);
}

$this->joinRelations($queryBuilder, $queryNameGenerator, $resourceClass, $forceEager, $fetchPartial, $queryBuilder->getRootAliases()[0], $options, $normalizationContext);
}

/**
Expand All @@ -105,7 +116,7 @@ public function applyToItem(QueryBuilder $queryBuilder, QueryNameGeneratorInterf
*
* @throws RuntimeException when the max number of joins has been reached
*/
private function joinRelations(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, bool $forceEager, bool $fetchPartial, string $parentAlias, array $propertyMetadataOptions = [], array $context = [], bool $wasLeftJoin = false, int &$joinCount = 0, int $currentDepth = null)
private function joinRelations(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, bool $forceEager, bool $fetchPartial, string $parentAlias, array $options = [], array $normalizationContext = [], bool $wasLeftJoin = false, int &$joinCount = 0, int $currentDepth = null)
{
if ($joinCount > $this->maxJoins) {
throw new RuntimeException('The total number of joined relations has exceeded the specified maximum. Raise the limit if necessary, or use the "max_depth" option of the Symfony serializer.');
Expand All @@ -116,14 +127,18 @@ private function joinRelations(QueryBuilder $queryBuilder, QueryNameGeneratorInt
$classMetadata = $entityManager->getClassMetadata($resourceClass);
$attributesMetadata = $this->classMetadataFactory ? $this->classMetadataFactory->getMetadataFor($resourceClass)->getAttributesMetadata() : null;

if (!empty($normalizationContext[AbstractNormalizer::GROUPS])) {
$options['serializer_groups'] = $normalizationContext[AbstractNormalizer::GROUPS];
}

foreach ($classMetadata->associationMappings as $association => $mapping) {
//Don't join if max depth is enabled and the current depth limit is reached
if (0 === $currentDepth && isset($context[AbstractObjectNormalizer::ENABLE_MAX_DEPTH])) {
if (0 === $currentDepth && isset($normalizationContext[AbstractObjectNormalizer::ENABLE_MAX_DEPTH])) {
continue;
}

try {
$propertyMetadata = $this->propertyMetadataFactory->create($resourceClass, $association, $propertyMetadataOptions);
$propertyMetadata = $this->propertyMetadataFactory->create($resourceClass, $association, $options);
} catch (PropertyNotFoundException $propertyNotFoundException) {
//skip properties not found
continue;
Expand All @@ -141,12 +156,12 @@ private function joinRelations(QueryBuilder $queryBuilder, QueryNameGeneratorInt
continue;
}

if (isset($context[AbstractNormalizer::ATTRIBUTES])) {
if ($inAttributes = isset($context[AbstractNormalizer::ATTRIBUTES][$association])) {
if (isset($normalizationContext[AbstractNormalizer::ATTRIBUTES])) {
if ($inAttributes = isset($normalizationContext[AbstractNormalizer::ATTRIBUTES][$association])) {
// prepare the child context
$context[AbstractNormalizer::ATTRIBUTES] = $context[AbstractNormalizer::ATTRIBUTES][$association];
$normalizationContext[AbstractNormalizer::ATTRIBUTES] = $normalizationContext[AbstractNormalizer::ATTRIBUTES][$association];
} else {
unset($context[AbstractNormalizer::ATTRIBUTES]);
unset($normalizationContext[AbstractNormalizer::ATTRIBUTES]);
}
} else {
$inAttributes = null;
Expand Down Expand Up @@ -176,7 +191,7 @@ private function joinRelations(QueryBuilder $queryBuilder, QueryNameGeneratorInt

if (true === $fetchPartial) {
try {
$this->addSelect($queryBuilder, $mapping['targetEntity'], $associationAlias, $propertyMetadataOptions);
$this->addSelect($queryBuilder, $mapping['targetEntity'], $associationAlias, $options);
} catch (ResourceClassNotFoundException $resourceClassNotFoundException) {
continue;
}
Expand All @@ -199,7 +214,7 @@ private function joinRelations(QueryBuilder $queryBuilder, QueryNameGeneratorInt
}
}

$this->joinRelations($queryBuilder, $queryNameGenerator, $mapping['targetEntity'], $forceEager, $fetchPartial, $associationAlias, $propertyMetadataOptions, $context, 'leftJoin' === $method, $joinCount, $currentDepth);
$this->joinRelations($queryBuilder, $queryNameGenerator, $mapping['targetEntity'], $forceEager, $fetchPartial, $associationAlias, $options, $normalizationContext, 'leftJoin' === $method, $joinCount, $currentDepth);
}
}

Expand Down Expand Up @@ -245,14 +260,10 @@ private function addSelect(QueryBuilder $queryBuilder, string $entity, string $a
* @param string $contextType normalization_context or denormalization_context
* @param array $options represents the operation name so that groups are the one of the specific operation
*/
private function getPropertyMetadataOptions(string $resourceClass, string $contextType, array $options): array
private function getNormalizationContext(string $resourceClass, string $contextType, array $options): array
{
$request = null;
if (null !== $this->requestStack && null !== $this->serializerContextBuilder) {
$request = $this->requestStack->getCurrentRequest();
}

if (null !== $this->serializerContextBuilder && null !== $request && !$request->attributes->get('_graphql')) {
if (null !== $this->requestStack && null !== $this->serializerContextBuilder && null !== $request = $this->requestStack->getCurrentRequest()) {
return $this->serializerContextBuilder->createFromRequest($request, 'normalization_context' === $contextType);
}

Expand All @@ -265,20 +276,6 @@ private function getPropertyMetadataOptions(string $resourceClass, string $conte
$context = $resourceMetadata->getAttribute($contextType);
}

return $context ?: [];
}

/**
* Gets serializer groups 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
*/
private function getSerializerGroups(array $options, array $context): array
{
if (!empty($context[AbstractNormalizer::GROUPS])) {
$options['serializer_groups'] = $context[AbstractNormalizer::GROUPS];
}

return $options;
return $context ?? [];
}
}