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

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