vendor/api-platform/core/src/Core/Bridge/Doctrine/Orm/Extension/EagerLoadingExtension.php line 88

Open in your IDE?
  1. <?php
  2. /*
  3.  * This file is part of the API Platform project.
  4.  *
  5.  * (c) Kévin Dunglas <dunglas@gmail.com>
  6.  *
  7.  * For the full copyright and license information, please view the LICENSE
  8.  * file that was distributed with this source code.
  9.  */
  10. declare(strict_types=1);
  11. namespace ApiPlatform\Core\Bridge\Doctrine\Orm\Extension;
  12. use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\EagerLoadingTrait;
  13. use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryBuilderHelper;
  14. use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryNameGeneratorInterface;
  15. use ApiPlatform\Core\Exception\InvalidArgumentException;
  16. use ApiPlatform\Core\Exception\PropertyNotFoundException;
  17. use ApiPlatform\Core\Exception\ResourceClassNotFoundException;
  18. use ApiPlatform\Core\Exception\RuntimeException;
  19. use ApiPlatform\Core\Metadata\Property\Factory\PropertyMetadataFactoryInterface;
  20. use ApiPlatform\Core\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface;
  21. use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface;
  22. use ApiPlatform\Core\Serializer\SerializerContextBuilderInterface;
  23. use Doctrine\ORM\Mapping\ClassMetadata;
  24. use Doctrine\ORM\Mapping\ClassMetadataInfo;
  25. use Doctrine\ORM\Query\Expr\Join;
  26. use Doctrine\ORM\Query\Expr\Select;
  27. use Doctrine\ORM\QueryBuilder;
  28. use Symfony\Component\HttpFoundation\RequestStack;
  29. use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactoryInterface;
  30. use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
  31. use Symfony\Component\Serializer\Normalizer\AbstractObjectNormalizer;
  32. /**
  33.  * Eager loads relations.
  34.  *
  35.  * @author Charles Sarrazin <charles@sarraz.in>
  36.  * @author Kévin Dunglas <dunglas@gmail.com>
  37.  * @author Antoine Bluchet <soyuka@gmail.com>
  38.  * @author Baptiste Meyer <baptiste.meyer@gmail.com>
  39.  */
  40. final class EagerLoadingExtension implements ContextAwareQueryCollectionExtensionInterfaceQueryItemExtensionInterface
  41. {
  42.     use EagerLoadingTrait;
  43.     private $propertyNameCollectionFactory;
  44.     private $propertyMetadataFactory;
  45.     private $classMetadataFactory;
  46.     private $maxJoins;
  47.     private $serializerContextBuilder;
  48.     private $requestStack;
  49.     public function __construct(PropertyNameCollectionFactoryInterface $propertyNameCollectionFactoryPropertyMetadataFactoryInterface $propertyMetadataFactoryResourceMetadataFactoryInterface $resourceMetadataFactoryint $maxJoins 30bool $forceEager trueRequestStack $requestStack nullSerializerContextBuilderInterface $serializerContextBuilder nullbool $fetchPartial falseClassMetadataFactoryInterface $classMetadataFactory null)
  50.     {
  51.         if (null !== $this->requestStack) {
  52.             @trigger_error(sprintf('Passing an instance of "%s" is deprecated since version 2.2 and will be removed in 3.0. Use the data provider\'s context instead.'RequestStack::class), \E_USER_DEPRECATED);
  53.         }
  54.         if (null !== $this->serializerContextBuilder) {
  55.             @trigger_error(sprintf('Passing an instance of "%s" is deprecated since version 2.2 and will be removed in 3.0. Use the data provider\'s context instead.'SerializerContextBuilderInterface::class), \E_USER_DEPRECATED);
  56.         }
  57.         $this->propertyNameCollectionFactory $propertyNameCollectionFactory;
  58.         $this->propertyMetadataFactory $propertyMetadataFactory;
  59.         $this->resourceMetadataFactory $resourceMetadataFactory;
  60.         $this->classMetadataFactory $classMetadataFactory;
  61.         $this->maxJoins $maxJoins;
  62.         $this->forceEager $forceEager;
  63.         $this->fetchPartial $fetchPartial;
  64.         $this->serializerContextBuilder $serializerContextBuilder;
  65.         $this->requestStack $requestStack;
  66.     }
  67.     public function applyToCollection(QueryBuilder $queryBuilderQueryNameGeneratorInterface $queryNameGeneratorstring $resourceClass nullstring $operationName null, array $context = [])
  68.     {
  69.         $this->apply(true$queryBuilder$queryNameGenerator$resourceClass$operationName$context);
  70.     }
  71.     /**
  72.      * {@inheritdoc}
  73.      *
  74.      * The context may contain serialization groups which helps defining joined entities that are readable.
  75.      */
  76.     public function applyToItem(QueryBuilder $queryBuilderQueryNameGeneratorInterface $queryNameGeneratorstring $resourceClass, array $identifiersstring $operationName null, array $context = [])
  77.     {
  78.         $this->apply(false$queryBuilder$queryNameGenerator$resourceClass$operationName$context);
  79.     }
  80.     private function apply(bool $collectionQueryBuilder $queryBuilderQueryNameGeneratorInterface $queryNameGenerator, ?string $resourceClass, ?string $operationName, array $context)
  81.     {
  82.         if (null === $resourceClass) {
  83.             throw new InvalidArgumentException('The "$resourceClass" parameter must not be null');
  84.         }
  85.         $options = [];
  86.         if (null !== $operationName) {
  87.             // TODO remove in 3.0
  88.             $options[($collection 'collection' 'item').'_operation_name'] = $operationName;
  89.         }
  90.         $operation null;
  91.         $forceEager $this->shouldOperationForceEager($resourceClass$options);
  92.         $fetchPartial $this->shouldOperationFetchPartial($resourceClass$options);
  93.         if (!isset($context['groups']) && !isset($context['attributes'])) {
  94.             $contextType = isset($context['api_denormalize']) ? 'denormalization_context' 'normalization_context';
  95.             if (null !== $this->requestStack && null !== $this->serializerContextBuilder && null !== $request $this->requestStack->getCurrentRequest()) {
  96.                 $context += $this->serializerContextBuilder->createFromRequest($request'normalization_context' === $contextType);
  97.             } else {
  98.                 $context += $this->getNormalizationContext($context['resource_class'] ?? $resourceClass$contextType$options);
  99.             }
  100.         }
  101.         if (empty($context[AbstractNormalizer::GROUPS]) && !isset($context[AbstractNormalizer::ATTRIBUTES])) {
  102.             return;
  103.         }
  104.         if (!empty($context[AbstractNormalizer::GROUPS])) {
  105.             $options['serializer_groups'] = (array) $context[AbstractNormalizer::GROUPS];
  106.         }
  107.         $this->joinRelations($queryBuilder$queryNameGenerator$resourceClass$forceEager$fetchPartial$queryBuilder->getRootAliases()[0], $options$context);
  108.     }
  109.     /**
  110.      * Joins relations to eager load.
  111.      *
  112.      * @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
  113.      * @param int  $joinCount    the number of joins
  114.      * @param int  $currentDepth the current max depth
  115.      *
  116.      * @throws RuntimeException when the max number of joins has been reached
  117.      */
  118.     private function joinRelations(QueryBuilder $queryBuilderQueryNameGeneratorInterface $queryNameGeneratorstring $resourceClassbool $forceEagerbool $fetchPartialstring $parentAlias, array $options = [], array $normalizationContext = [], bool $wasLeftJoin falseint &$joinCount 0int $currentDepth nullstring $parentAssociation null)
  119.     {
  120.         if ($joinCount $this->maxJoins) {
  121.             throw new RuntimeException('The total number of joined relations has exceeded the specified maximum. Raise the limit if necessary with the "api_platform.eager_loading.max_joins" configuration key (https://api-platform.com/docs/core/performance/#eager-loading), or limit the maximum serialization depth using the "enable_max_depth" option of the Symfony serializer (https://symfony.com/doc/current/components/serializer.html#handling-serialization-depth).');
  122.         }
  123.         $currentDepth $currentDepth $currentDepth $currentDepth;
  124.         $entityManager $queryBuilder->getEntityManager();
  125.         $classMetadata $entityManager->getClassMetadata($resourceClass);
  126.         $attributesMetadata $this->classMetadataFactory $this->classMetadataFactory->getMetadataFor($resourceClass)->getAttributesMetadata() : null;
  127.         foreach ($classMetadata->associationMappings as $association => $mapping) {
  128.             // Don't join if max depth is enabled and the current depth limit is reached
  129.             if (=== $currentDepth && ($normalizationContext[AbstractObjectNormalizer::ENABLE_MAX_DEPTH] ?? false)) {
  130.                 continue;
  131.             }
  132.             try {
  133.                 $propertyMetadata $this->propertyMetadataFactory->create($resourceClass$association$options);
  134.             } catch (PropertyNotFoundException $propertyNotFoundException) {
  135.                 // skip properties not found
  136.                 continue;
  137.                 // @phpstan-ignore-next-line indeed this can be thrown by the SerializerPropertyMetadataFactory
  138.             } catch (ResourceClassNotFoundException $resourceClassNotFoundException) {
  139.                 // skip associations that are not resource classes
  140.                 continue;
  141.             }
  142.             if (
  143.                 // Always skip extra lazy associations
  144.                 ClassMetadataInfo::FETCH_EXTRA_LAZY === $mapping['fetch']
  145.                 // We don't want to interfere with doctrine on this association
  146.                 || (false === $forceEager && ClassMetadataInfo::FETCH_EAGER !== $mapping['fetch'])
  147.             ) {
  148.                 continue;
  149.             }
  150.             // prepare the child context
  151.             $childNormalizationContext $normalizationContext;
  152.             if (isset($normalizationContext[AbstractNormalizer::ATTRIBUTES])) {
  153.                 if ($inAttributes = isset($normalizationContext[AbstractNormalizer::ATTRIBUTES][$association])) {
  154.                     $childNormalizationContext[AbstractNormalizer::ATTRIBUTES] = $normalizationContext[AbstractNormalizer::ATTRIBUTES][$association];
  155.                 }
  156.             } else {
  157.                 $inAttributes null;
  158.             }
  159.             $fetchEager null;
  160.             if (
  161.                 (null === $fetchEager $propertyMetadata->getAttribute('fetch_eager'))
  162.                 && (null !== $fetchEager $propertyMetadata->getAttribute('fetchEager'))
  163.             ) {
  164.                 @trigger_error('The "fetchEager" attribute is deprecated since 2.3. Please use "fetch_eager" instead.', \E_USER_DEPRECATED);
  165.             }
  166.             if (false === $fetchEager) {
  167.                 continue;
  168.             }
  169.             if (true !== $fetchEager && (false === $propertyMetadata->isReadable() || false === $inAttributes)) {
  170.                 continue;
  171.             }
  172.             // Avoid joining back to the parent that we just came from, but only on *ToOne relations
  173.             if (
  174.                 null !== $parentAssociation
  175.                 && isset($mapping['inversedBy'])
  176.                 && $mapping['inversedBy'] === $parentAssociation
  177.                 && $mapping['type'] & ClassMetadata::TO_ONE
  178.             ) {
  179.                 continue;
  180.             }
  181.             $existingJoin QueryBuilderHelper::getExistingJoin($queryBuilder$parentAlias$association);
  182.             if (null !== $existingJoin) {
  183.                 $associationAlias $existingJoin->getAlias();
  184.                 $isLeftJoin Join::LEFT_JOIN === $existingJoin->getJoinType();
  185.             } else {
  186.                 $isNullable $mapping['joinColumns'][0]['nullable'] ?? true;
  187.                 $isLeftJoin false !== $wasLeftJoin || true === $isNullable;
  188.                 $method $isLeftJoin 'leftJoin' 'innerJoin';
  189.                 $associationAlias $queryNameGenerator->generateJoinAlias($association);
  190.                 $queryBuilder->{$method}(sprintf('%s.%s'$parentAlias$association), $associationAlias);
  191.                 ++$joinCount;
  192.             }
  193.             if (true === $fetchPartial) {
  194.                 try {
  195.                     $this->addSelect($queryBuilder$mapping['targetEntity'], $associationAlias$options);
  196.                 } catch (ResourceClassNotFoundException $resourceClassNotFoundException) {
  197.                     continue;
  198.                 }
  199.             } else {
  200.                 $this->addSelectOnce($queryBuilder$associationAlias);
  201.             }
  202.             // Avoid recursive joins for self-referencing relations
  203.             if ($mapping['targetEntity'] === $resourceClass) {
  204.                 continue;
  205.             }
  206.             // Only join the relation's relations recursively if it's a readableLink
  207.             if (true !== $fetchEager && (true !== $propertyMetadata->isReadableLink())) {
  208.                 continue;
  209.             }
  210.             if (isset($attributesMetadata[$association])) {
  211.                 $maxDepth $attributesMetadata[$association]->getMaxDepth();
  212.                 // The current depth is the lowest max depth available in the ancestor tree.
  213.                 if (null !== $maxDepth && (null === $currentDepth || $maxDepth $currentDepth)) {
  214.                     $currentDepth $maxDepth;
  215.                 }
  216.             }
  217.             $this->joinRelations($queryBuilder$queryNameGenerator$mapping['targetEntity'], $forceEager$fetchPartial$associationAlias$options$childNormalizationContext$isLeftJoin$joinCount$currentDepth$association);
  218.         }
  219.     }
  220.     private function addSelect(QueryBuilder $queryBuilderstring $entitystring $associationAlias, array $propertyMetadataOptions)
  221.     {
  222.         $select = [];
  223.         $entityManager $queryBuilder->getEntityManager();
  224.         $targetClassMetadata $entityManager->getClassMetadata($entity);
  225.         if (!empty($targetClassMetadata->subClasses)) {
  226.             $this->addSelectOnce($queryBuilder$associationAlias);
  227.             return;
  228.         }
  229.         foreach ($this->propertyNameCollectionFactory->create($entity) as $property) {
  230.             $propertyMetadata $this->propertyMetadataFactory->create($entity$property$propertyMetadataOptions);
  231.             if (true === $propertyMetadata->isIdentifier()) {
  232.                 $select[] = $property;
  233.                 continue;
  234.             }
  235.             // If it's an embedded property see below
  236.             if (!\array_key_exists($property$targetClassMetadata->embeddedClasses)) {
  237.                 $isFetchable $propertyMetadata->getAttribute('fetchable');
  238.                 // the field test allows to add methods to a Resource which do not reflect real database fields
  239.                 if ($targetClassMetadata->hasField($property) && (true === $isFetchable || $propertyMetadata->isReadable())) {
  240.                     $select[] = $property;
  241.                 }
  242.                 continue;
  243.             }
  244.             // It's an embedded property, select relevant subfields
  245.             foreach ($this->propertyNameCollectionFactory->create($targetClassMetadata->embeddedClasses[$property]['class']) as $embeddedProperty) {
  246.                 $isFetchable $propertyMetadata->getAttribute('fetchable');
  247.                 $propertyMetadata $this->propertyMetadataFactory->create($entity$property$propertyMetadataOptions);
  248.                 $propertyName "$property.$embeddedProperty";
  249.                 if ($targetClassMetadata->hasField($propertyName) && (true === $isFetchable || $propertyMetadata->isReadable())) {
  250.                     $select[] = $propertyName;
  251.                 }
  252.             }
  253.         }
  254.         $queryBuilder->addSelect(sprintf('partial %s.{%s}'$associationAliasimplode(','$select)));
  255.     }
  256.     private function addSelectOnce(QueryBuilder $queryBuilderstring $alias)
  257.     {
  258.         $existingSelects array_reduce($queryBuilder->getDQLPart('select') ?? [], function ($existing$dqlSelect) {
  259.             return ($dqlSelect instanceof Select) ? array_merge($existing$dqlSelect->getParts()) : $existing;
  260.         }, []);
  261.         if (!\in_array($alias$existingSelectstrue)) {
  262.             $queryBuilder->addSelect($alias);
  263.         }
  264.     }
  265.     /**
  266.      * Gets the serializer context.
  267.      *
  268.      * @param string $contextType normalization_context or denormalization_context
  269.      * @param array  $options     represents the operation name so that groups are the one of the specific operation
  270.      */
  271.     private function getNormalizationContext(string $resourceClassstring $contextType, array $options): array
  272.     {
  273.         if (null !== $this->requestStack && null !== $this->serializerContextBuilder && null !== $request $this->requestStack->getCurrentRequest()) {
  274.             return $this->serializerContextBuilder->createFromRequest($request'normalization_context' === $contextType);
  275.         }
  276.         $resourceMetadata $this->resourceMetadataFactory->create($resourceClass);
  277.         if (isset($options['collection_operation_name'])) {
  278.             $context $resourceMetadata->getCollectionOperationAttribute($options['collection_operation_name'], $contextTypenulltrue);
  279.         } elseif (isset($options['item_operation_name'])) {
  280.             $context $resourceMetadata->getItemOperationAttribute($options['item_operation_name'], $contextTypenulltrue);
  281.         } else {
  282.             $context $resourceMetadata->getAttribute($contextType);
  283.         }
  284.         return $context ?? [];
  285.     }
  286. }