vendor/api-platform/core/src/Core/Bridge/Doctrine/Orm/SubresourceDataProvider.php line 67

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;
  12. use ApiPlatform\Core\Bridge\Doctrine\Common\Util\IdentifierManagerTrait;
  13. use ApiPlatform\Core\Bridge\Doctrine\Orm\Extension\QueryCollectionExtensionInterface as LegacyQueryCollectionExtensionInterface;
  14. use ApiPlatform\Core\Bridge\Doctrine\Orm\Extension\QueryItemExtensionInterface as LegacyQueryItemExtensionInterface;
  15. use ApiPlatform\Core\Bridge\Doctrine\Orm\Extension\QueryResultCollectionExtensionInterface as LegacyQueryResultCollectionExtensionInterface;
  16. use ApiPlatform\Core\Bridge\Doctrine\Orm\Extension\QueryResultItemExtensionInterface as LegacyQueryResultItemExtensionInterface;
  17. use ApiPlatform\Core\DataProvider\SubresourceDataProviderInterface;
  18. use ApiPlatform\Core\Identifier\IdentifierConverterInterface;
  19. use ApiPlatform\Core\Metadata\Property\Factory\PropertyMetadataFactoryInterface;
  20. use ApiPlatform\Core\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface;
  21. use ApiPlatform\Doctrine\Orm\Extension\FilterEagerLoadingExtension;
  22. use ApiPlatform\Doctrine\Orm\Extension\QueryCollectionExtensionInterface;
  23. use ApiPlatform\Doctrine\Orm\Extension\QueryItemExtensionInterface;
  24. use ApiPlatform\Doctrine\Orm\Extension\QueryResultCollectionExtensionInterface;
  25. use ApiPlatform\Doctrine\Orm\Extension\QueryResultItemExtensionInterface;
  26. use ApiPlatform\Doctrine\Orm\Util\QueryNameGenerator;
  27. use ApiPlatform\Exception\ResourceClassNotSupportedException;
  28. use ApiPlatform\Exception\RuntimeException;
  29. use Doctrine\ORM\EntityManagerInterface;
  30. use Doctrine\ORM\Mapping\ClassMetadataInfo;
  31. use Doctrine\ORM\QueryBuilder;
  32. use Doctrine\Persistence\ManagerRegistry;
  33. /**
  34. * Subresource data provider for the Doctrine ORM.
  35. *
  36. * @author Antoine Bluchet <soyuka@gmail.com>
  37. */
  38. final class SubresourceDataProvider implements SubresourceDataProviderInterface
  39. {
  40. use IdentifierManagerTrait;
  41. private $managerRegistry;
  42. private $collectionExtensions;
  43. private $itemExtensions;
  44. /**
  45. * @param LegacyQueryCollectionExtensionInterface[]|QueryCollectionExtensionInterface[] $collectionExtensions
  46. * @param LegacyQueryItemExtensionInterface[]|QueryItemExtensionInterface[] $itemExtensions
  47. */
  48. public function __construct(ManagerRegistry $managerRegistry, PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, iterable $collectionExtensions = [], iterable $itemExtensions = [])
  49. {
  50. $this->managerRegistry = $managerRegistry;
  51. $this->propertyNameCollectionFactory = $propertyNameCollectionFactory;
  52. $this->propertyMetadataFactory = $propertyMetadataFactory;
  53. $this->collectionExtensions = $collectionExtensions;
  54. $this->itemExtensions = $itemExtensions;
  55. }
  56. /**
  57. * @throws RuntimeException
  58. */
  59. public function getSubresource(string $resourceClass, array $identifiers, array $context, string $operationName = null)
  60. {
  61. $manager = $this->managerRegistry->getManagerForClass($resourceClass);
  62. if (null === $manager) {
  63. throw new ResourceClassNotSupportedException(sprintf('The object manager associated with the "%s" resource class cannot be retrieved.', $resourceClass));
  64. }
  65. $repository = $manager->getRepository($resourceClass);
  66. if (!method_exists($repository, 'createQueryBuilder')) {
  67. throw new RuntimeException('The repository class must have a "createQueryBuilder" method.');
  68. }
  69. if (!isset($context['identifiers'], $context['property'])) {
  70. throw new ResourceClassNotSupportedException('The given resource class is not a subresource.');
  71. }
  72. $queryNameGenerator = new QueryNameGenerator();
  73. /*
  74. * The following recursively translates to this pseudo-dql:
  75. *
  76. * SELECT thirdLevel WHERE thirdLevel IN (
  77. * SELECT thirdLevel FROM relatedDummies WHERE relatedDummies = ? AND relatedDummies IN (
  78. * SELECT relatedDummies FROM Dummy WHERE Dummy = ?
  79. * )
  80. * )
  81. *
  82. * By using subqueries, we're forcing the SQL execution plan to go through indexes on doctrine identifiers.
  83. */
  84. $queryBuilder = $this->buildQuery($identifiers, $context, $queryNameGenerator, $repository->createQueryBuilder($alias = 'o'), $alias, \count($context['identifiers']));
  85. if (true === $context['collection']) {
  86. foreach ($this->collectionExtensions as $extension) {
  87. // We don't need this anymore because we already made sub queries to ensure correct results
  88. if ($extension instanceof FilterEagerLoadingExtension) {
  89. continue;
  90. }
  91. if ($extension instanceof LegacyQueryCollectionExtensionInterface) {
  92. $extension->applyToCollection($queryBuilder, $queryNameGenerator, $resourceClass, $operationName, $context); // @phpstan-ignore-line see context aware
  93. } elseif ($extension instanceof QueryCollectionExtensionInterface) {
  94. $extension->applyToCollection($queryBuilder, $queryNameGenerator, $resourceClass, $context['operation'] ?? null, $context);
  95. }
  96. if ($extension instanceof LegacyQueryResultCollectionExtensionInterface && $extension->supportsResult($resourceClass, $operationName, $context)) { // @phpstan-ignore-line see context aware
  97. return $extension->getResult($queryBuilder, $resourceClass, $operationName, $context); // @phpstan-ignore-line see context aware
  98. }
  99. if ($extension instanceof QueryResultCollectionExtensionInterface && $extension->supportsResult($resourceClass, $context['operation'] ?? null, $context)) {
  100. return $extension->getResult($queryBuilder, $resourceClass, $context['operation'] ?? null, $context);
  101. }
  102. }
  103. } else {
  104. foreach ($this->itemExtensions as $extension) {
  105. if ($extension instanceof LegacyQueryItemExtensionInterface) {
  106. $extension->applyToItem($queryBuilder, $queryNameGenerator, $resourceClass, $identifiers, $operationName, $context);
  107. } elseif ($extension instanceof QueryItemExtensionInterface) {
  108. $extension->applyToItem($queryBuilder, $queryNameGenerator, $resourceClass, $identifiers, $context['operation'] ?? null, $context);
  109. }
  110. if ($extension instanceof LegacyQueryResultItemExtensionInterface && $extension->supportsResult($resourceClass, $operationName, $context)) { // @phpstan-ignore-line see context aware
  111. return $extension->getResult($queryBuilder, $resourceClass, $operationName, $context); // @phpstan-ignore-line see context aware
  112. }
  113. if ($extension instanceof QueryResultItemExtensionInterface && $extension->supportsResult($resourceClass, $context['operation'] ?? null, $context)) {
  114. return $extension->getResult($queryBuilder, $resourceClass, $context['operation'], $context);
  115. }
  116. }
  117. }
  118. $query = $queryBuilder->getQuery();
  119. return $context['collection'] ? $query->getResult() : $query->getOneOrNullResult();
  120. }
  121. /**
  122. * @throws RuntimeException
  123. */
  124. private function buildQuery(array $identifiers, array $context, QueryNameGenerator $queryNameGenerator, QueryBuilder $previousQueryBuilder, string $previousAlias, int $remainingIdentifiers, QueryBuilder $topQueryBuilder = null): QueryBuilder
  125. {
  126. if ($remainingIdentifiers <= 0) {
  127. return $previousQueryBuilder;
  128. }
  129. $topQueryBuilder = $topQueryBuilder ?? $previousQueryBuilder;
  130. if (\is_string(key($context['identifiers']))) {
  131. $contextIdentifiers = array_keys($context['identifiers']);
  132. $identifier = $contextIdentifiers[$remainingIdentifiers - 1];
  133. $identifierResourceClass = $context['identifiers'][$identifier][0];
  134. $previousAssociationProperty = $contextIdentifiers[$remainingIdentifiers] ?? $context['property'];
  135. } else {
  136. @trigger_error('Identifiers should match the convention introduced in ADR 0001-resource-identifiers, this behavior will be removed in 3.0.', \E_USER_DEPRECATED);
  137. [$identifier, $identifierResourceClass] = $context['identifiers'][$remainingIdentifiers - 1];
  138. $previousAssociationProperty = $context['identifiers'][$remainingIdentifiers][0] ?? $context['property'];
  139. }
  140. $manager = $this->managerRegistry->getManagerForClass($identifierResourceClass);
  141. if (!$manager instanceof EntityManagerInterface) {
  142. throw new RuntimeException("The manager for $identifierResourceClass must be an EntityManager.");
  143. }
  144. $classMetadata = $manager->getClassMetadata($identifierResourceClass);
  145. if (!$classMetadata instanceof ClassMetadataInfo) {
  146. throw new RuntimeException("The class metadata for $identifierResourceClass must be an instance of ClassMetadataInfo.");
  147. }
  148. $qb = $manager->createQueryBuilder();
  149. $alias = $queryNameGenerator->generateJoinAlias($identifier);
  150. $normalizedIdentifiers = [];
  151. if (isset($identifiers[$identifier])) {
  152. // if it's an array it's already normalized, the IdentifierManagerTrait is deprecated
  153. if ($context[IdentifierConverterInterface::HAS_IDENTIFIER_CONVERTER] ?? false) {
  154. $normalizedIdentifiers = $identifiers[$identifier];
  155. } else {
  156. $normalizedIdentifiers = $this->normalizeIdentifiers($identifiers[$identifier], $manager, $identifierResourceClass);
  157. }
  158. }
  159. if ($classMetadata->hasAssociation($previousAssociationProperty)) {
  160. $relationType = $classMetadata->getAssociationMapping($previousAssociationProperty)['type'];
  161. switch ($relationType) {
  162. // MANY_TO_MANY relations need an explicit join so that the identifier part can be retrieved
  163. case ClassMetadataInfo::MANY_TO_MANY:
  164. $joinAlias = $queryNameGenerator->generateJoinAlias($previousAssociationProperty);
  165. $qb->select($joinAlias)
  166. ->from($identifierResourceClass, $alias)
  167. ->innerJoin("$alias.$previousAssociationProperty", $joinAlias);
  168. break;
  169. case ClassMetadataInfo::ONE_TO_MANY:
  170. $mappedBy = $classMetadata->getAssociationMapping($previousAssociationProperty)['mappedBy'];
  171. $previousAlias = "$previousAlias.$mappedBy";
  172. $qb->select($alias)
  173. ->from($identifierResourceClass, $alias);
  174. break;
  175. case ClassMetadataInfo::ONE_TO_ONE:
  176. $association = $classMetadata->getAssociationMapping($previousAssociationProperty);
  177. if (!isset($association['mappedBy'])) {
  178. $qb->select("IDENTITY($alias.$previousAssociationProperty)")
  179. ->from($identifierResourceClass, $alias);
  180. break;
  181. }
  182. $mappedBy = $association['mappedBy'];
  183. $previousAlias = "$previousAlias.$mappedBy";
  184. $qb->select($alias)
  185. ->from($identifierResourceClass, $alias);
  186. break;
  187. default:
  188. $qb->select("IDENTITY($alias.$previousAssociationProperty)")
  189. ->from($identifierResourceClass, $alias);
  190. }
  191. } elseif ($classMetadata->isIdentifier($previousAssociationProperty)) {
  192. $qb->select($alias)
  193. ->from($identifierResourceClass, $alias);
  194. }
  195. $isLeaf = 1 === $remainingIdentifiers;
  196. // Add where clause for identifiers
  197. foreach ($normalizedIdentifiers as $key => $value) {
  198. $placeholder = $queryNameGenerator->generateParameterName($key);
  199. $topQueryBuilder->setParameter($placeholder, $value, (string) $classMetadata->getTypeOfField($key));
  200. // Optimization: add where clause for identifiers, but not via a WHERE ... IN ( ...subquery... ).
  201. // Instead we use a direct identifier equality clause, to speed things up when dealing with large tables.
  202. // We may do so if there is no more recursion levels from here, and if relation allows it.
  203. $association = $classMetadata->hasAssociation($previousAssociationProperty) ? $classMetadata->getAssociationMapping($previousAssociationProperty) : [];
  204. $oneToOneBidirectional = isset($association['inversedBy']) && ClassMetadataInfo::ONE_TO_ONE === $association['type'];
  205. $oneToManyBidirectional = isset($association['mappedBy']) && ClassMetadataInfo::ONE_TO_MANY === $association['type'];
  206. if ($isLeaf && $oneToOneBidirectional) {
  207. $joinAlias = $queryNameGenerator->generateJoinAlias($association['inversedBy']);
  208. return $previousQueryBuilder->innerJoin("$previousAlias.{$association['inversedBy']}", $joinAlias)
  209. ->andWhere("$joinAlias.$key = :$placeholder");
  210. }
  211. if ($isLeaf && $oneToManyBidirectional && \in_array($key, $classMetadata->getIdentifier(), true)) {
  212. return $previousQueryBuilder->andWhere("IDENTITY($previousAlias) = :$placeholder");
  213. }
  214. $qb->andWhere("$alias.$key = :$placeholder");
  215. }
  216. // Recurse queries
  217. $qb = $this->buildQuery($identifiers, $context, $queryNameGenerator, $qb, $alias, --$remainingIdentifiers, $topQueryBuilder);
  218. return $previousQueryBuilder->andWhere($qb->expr()->in($previousAlias, $qb->getDQL()));
  219. }
  220. }