diff --git a/composer.json b/composer.json index 747e8cc2bb6..00c64920f60 100644 --- a/composer.json +++ b/composer.json @@ -44,7 +44,6 @@ "jangregor/phpstan-prophecy": "^0.3", "justinrainbow/json-schema": "^5.0", "nelmio/api-doc-bundle": "^2.13.3", - "php-mock/php-mock-phpunit": "^2.0", "phpdocumentor/reflection-docblock": "^3.0 || ^4.0", "phpdocumentor/type-resolver": "^0.3 || ^0.4", "phpspec/prophecy": "^1.8", diff --git a/phpstan.neon.dist b/phpstan.neon.dist index 9e1fb88960a..d906bbcda6a 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -67,6 +67,9 @@ parameters: path: %currentWorkingDirectory%/src/Test/DoctrineMongoDbOdmFilterTestCase.php - '#Method ApiPlatform\\Core\\(Serializer\\Abstract|JsonApi\\Serializer\\)ItemNormalizer::normalizeRelation\(\) should return array\|string but returns array\|bool\|float\|int\|string\.#' - '#Method ApiPlatform\\Core\\Util\\RequestParser::parseRequestParams\(\) should return array but returns array\|false\.#' + - + message: '#Method ApiPlatform\\Core\\Bridge\\Doctrine\\Orm\\Util\\QueryBuilderHelper::mapJoinAliases() should return array\|string> but returns array\.#' + path: %currentWorkingDirectory%/src/Bridge/Doctrine/Orm/Util/QueryBuilderHelper.php - "#Call to method PHPUnit\\\\Framework\\\\Assert::assertSame\\(\\) with array\\('(collection_context|item_context|subresource_context)'\\) and array\\|bool\\|float\\|int\\|string\\|null will always evaluate to false\\.#" # https://github.com/symfony/symfony/pull/30535 - diff --git a/src/Bridge/Doctrine/Orm/Extension/PaginationExtension.php b/src/Bridge/Doctrine/Orm/Extension/PaginationExtension.php index 7647d07a6e0..53cb33b587a 100644 --- a/src/Bridge/Doctrine/Orm/Extension/PaginationExtension.php +++ b/src/Bridge/Doctrine/Orm/Extension/PaginationExtension.php @@ -151,8 +151,8 @@ public function getResult(QueryBuilder $queryBuilder, string $resourceClass = nu $query->setHint(CountWalker::HINT_DISTINCT, false); } - $doctrineOrmPaginator = new DoctrineOrmPaginator($query, $this->useFetchJoinCollection($queryBuilder, $resourceClass, $operationName)); - $doctrineOrmPaginator->setUseOutputWalkers($this->useOutputWalkers($queryBuilder)); + $doctrineOrmPaginator = new DoctrineOrmPaginator($query, $this->shouldDoctrinePaginatorFetchJoinCollection($queryBuilder, $resourceClass, $operationName)); + $doctrineOrmPaginator->setUseOutputWalkers($this->shouldDoctrinePaginatorUseOutputWalkers($queryBuilder, $resourceClass, $operationName)); if (null === $this->requestStack) { $isPartialEnabled = $this->pagination->isPartialEnabled($resourceClass, $operationName, $context); @@ -276,64 +276,82 @@ private function getPaginationParameter(Request $request, string $parameterName, } /** - * Determines whether the Paginator should fetch join collections, if the root entity uses composite identifiers it should not. - * - * @see https://github.com/doctrine/doctrine2/issues/2910 + * Determines the value of the $fetchJoinCollection argument passed to the Doctrine ORM Paginator. */ - private function useFetchJoinCollection(QueryBuilder $queryBuilder, string $resourceClass = null, string $operationName = null): bool + private function shouldDoctrinePaginatorFetchJoinCollection(QueryBuilder $queryBuilder, string $resourceClass = null, string $operationName = null): bool { + if (null !== $resourceClass) { + $resourceMetadata = $this->resourceMetadataFactory->create($resourceClass); + + if (null !== $fetchJoinCollection = $resourceMetadata->getCollectionOperationAttribute($operationName, 'pagination_fetch_join_collection', null, true)) { + return $fetchJoinCollection; + } + } + + /* + * "Cannot count query which selects two FROM components, cannot make distinction" + * + * @see https://github.com/doctrine/orm/blob/v2.6.3/lib/Doctrine/ORM/Tools/Pagination/WhereInWalker.php#L81 + * @see https://github.com/doctrine/doctrine2/issues/2910 + */ if (QueryChecker::hasRootEntityWithCompositeIdentifier($queryBuilder, $this->managerRegistry)) { return false; } - if (null === $resourceClass) { + if (QueryChecker::hasJoinedToManyAssociation($queryBuilder, $this->managerRegistry)) { return true; } - $resourceMetadata = $this->resourceMetadataFactory->create($resourceClass); - - return $resourceMetadata->getCollectionOperationAttribute($operationName, 'pagination_fetch_join_collection', true, true); + // disable $fetchJoinCollection by default (performance) + return false; } /** - * Determines whether output walkers should be used. + * Determines whether the Doctrine ORM Paginator should use output walkers. */ - private function useOutputWalkers(QueryBuilder $queryBuilder): bool + private function shouldDoctrinePaginatorUseOutputWalkers(QueryBuilder $queryBuilder, string $resourceClass = null, string $operationName = null): bool { + if (null !== $resourceClass) { + $resourceMetadata = $this->resourceMetadataFactory->create($resourceClass); + + if (null !== $useOutputWalkers = $resourceMetadata->getCollectionOperationAttribute($operationName, 'pagination_use_output_walkers', null, true)) { + return $useOutputWalkers; + } + } + /* * "Cannot count query that uses a HAVING clause. Use the output walkers for pagination" * - * @see https://github.com/doctrine/doctrine2/blob/900b55d16afdcdeb5100d435a7166d3a425b9873/lib/Doctrine/ORM/Tools/Pagination/CountWalker.php#L50 + * @see https://github.com/doctrine/orm/blob/v2.6.3/lib/Doctrine/ORM/Tools/Pagination/CountWalker.php#L56 */ if (QueryChecker::hasHavingClause($queryBuilder)) { return true; } /* - * "Paginating an entity with foreign key as identifier only works when using the Output Walkers. Call Paginator#setUseOutputWalkers(true) before iterating the paginator." + * "Cannot count query which selects two FROM components, cannot make distinction" * - * @see https://github.com/doctrine/doctrine2/blob/900b55d16afdcdeb5100d435a7166d3a425b9873/lib/Doctrine/ORM/Tools/Pagination/LimitSubqueryWalker.php#L87 + * @see https://github.com/doctrine/orm/blob/v2.6.3/lib/Doctrine/ORM/Tools/Pagination/CountWalker.php#L64 */ - if (QueryChecker::hasRootEntityWithForeignKeyIdentifier($queryBuilder, $this->managerRegistry)) { + if (QueryChecker::hasRootEntityWithCompositeIdentifier($queryBuilder, $this->managerRegistry)) { return true; } /* - * "Cannot select distinct identifiers from query with LIMIT and ORDER BY on a column from a fetch joined to-many association. Use output walkers." + * "Paginating an entity with foreign key as identifier only works when using the Output Walkers. Call Paginator#setUseOutputWalkers(true) before iterating the paginator." * - * @see https://github.com/doctrine/doctrine2/blob/900b55d16afdcdeb5100d435a7166d3a425b9873/lib/Doctrine/ORM/Tools/Pagination/LimitSubqueryWalker.php#L149 + * @see https://github.com/doctrine/orm/blob/v2.6.3/lib/Doctrine/ORM/Tools/Pagination/LimitSubqueryWalker.php#L77 */ - if ( - QueryChecker::hasMaxResults($queryBuilder) && - QueryChecker::hasOrderByOnToManyJoin($queryBuilder, $this->managerRegistry) - ) { + if (QueryChecker::hasRootEntityWithForeignKeyIdentifier($queryBuilder, $this->managerRegistry)) { return true; } /* - * When using composite identifiers pagination will need Output walkers + * "Cannot select distinct identifiers from query with LIMIT and ORDER BY on a column from a fetch joined to-many association. Use output walkers." + * + * @see https://github.com/doctrine/orm/blob/v2.6.3/lib/Doctrine/ORM/Tools/Pagination/LimitSubqueryWalker.php#L150 */ - if (QueryChecker::hasRootEntityWithCompositeIdentifier($queryBuilder, $this->managerRegistry)) { + if (QueryChecker::hasMaxResults($queryBuilder) && QueryChecker::hasOrderByOnFetchJoinedToManyAssociation($queryBuilder, $this->managerRegistry)) { return true; } diff --git a/src/Bridge/Doctrine/Orm/Util/QueryBuilderHelper.php b/src/Bridge/Doctrine/Orm/Util/QueryBuilderHelper.php index 863c4dcb334..240c416d273 100644 --- a/src/Bridge/Doctrine/Orm/Util/QueryBuilderHelper.php +++ b/src/Bridge/Doctrine/Orm/Util/QueryBuilderHelper.php @@ -13,6 +13,7 @@ namespace ApiPlatform\Core\Bridge\Doctrine\Orm\Util; +use Doctrine\Common\Persistence\ManagerRegistry; use Doctrine\ORM\Query\Expr\Join; use Doctrine\ORM\QueryBuilder; @@ -28,7 +29,7 @@ private function __construct() } /** - * Adds a join to the queryBuilder if none exists. + * Adds a join to the QueryBuilder if none exists. */ public static function addJoinOnce(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $alias, string $association, string $joinType = null, string $conditionType = null, string $condition = null, string $originAlias = null): string { @@ -51,7 +52,113 @@ public static function addJoinOnce(QueryBuilder $queryBuilder, QueryNameGenerato } /** - * Get the existing join from queryBuilder DQL parts. + * Gets the entity class name by an alias used in the QueryBuilder. + */ + public static function getEntityClassByAlias(string $alias, QueryBuilder $queryBuilder, ManagerRegistry $managerRegistry): string + { + if (!\in_array($alias, $queryBuilder->getAllAliases(), true)) { + throw new \LogicException(sprintf('The alias "%s" does not exist in the QueryBuilder.', $alias)); + } + + $rootAliasMap = self::mapRootAliases($queryBuilder->getRootAliases(), $queryBuilder->getRootEntities()); + + if (isset($rootAliasMap[$alias])) { + return $rootAliasMap[$alias]; + } + + $metadata = null; + + foreach (self::traverseJoins($alias, $queryBuilder, $managerRegistry) as [$currentMetadata]) { + $metadata = $currentMetadata; + } + + if (null === $metadata) { + throw new \LogicException(sprintf('The alias "%s" does not exist in the QueryBuilder.', $alias)); + } + + return $metadata->getName(); + } + + /** + * Finds the root alias for an alias used in the QueryBuilder. + */ + public static function findRootAlias(string $alias, QueryBuilder $queryBuilder): string + { + if (\in_array($alias, $queryBuilder->getRootAliases(), true)) { + return $alias; + } + + foreach ($queryBuilder->getDQLPart('join') as $rootAlias => $joins) { + foreach ($joins as $join) { + if ($alias === $join->getAlias()) { + return $rootAlias; + } + } + } + + throw new \LogicException(sprintf('The alias "%s" does not exist in the QueryBuilder.', $alias)); + } + + /** + * Traverses through the joins for an alias used in the QueryBuilder. + * + * @return \Generator + */ + public static function traverseJoins(string $alias, QueryBuilder $queryBuilder, ManagerRegistry $managerRegistry): \Generator + { + $rootAliasMap = self::mapRootAliases($queryBuilder->getRootAliases(), $queryBuilder->getRootEntities()); + + $joinParts = $queryBuilder->getDQLPart('join'); + $rootAlias = self::findRootAlias($alias, $queryBuilder); + + $joinAliasMap = self::mapJoinAliases($joinParts[$rootAlias]); + + $aliasMap = array_merge($rootAliasMap, $joinAliasMap); + + $apexEntityClass = null; + $associationStack = []; + $aliasStack = []; + $currentAlias = $alias; + + while (null === $apexEntityClass) { + if (!isset($aliasMap[$currentAlias])) { + throw new \LogicException(sprintf('Unknown alias "%s".', $currentAlias)); + } + + if (\is_string($aliasMap[$currentAlias])) { + $aliasStack[] = $currentAlias; + $apexEntityClass = $aliasMap[$currentAlias]; + } else { + [$parentAlias, $association] = $aliasMap[$currentAlias]; + + $associationStack[] = $association; + $aliasStack[] = $currentAlias; + $currentAlias = $parentAlias; + } + } + + $entityClass = $apexEntityClass; + + while (null !== ($alias = array_pop($aliasStack))) { + $metadata = $managerRegistry + ->getManagerForClass($entityClass) + ->getClassMetadata($entityClass); + + $association = array_pop($associationStack); + + yield $alias => [ + $metadata, + $association, + ]; + + if (null !== $association) { + $entityClass = $metadata->getAssociationTargetClass($association); + } + } + } + + /** + * Gets the existing join from QueryBuilder DQL parts. */ private static function getExistingJoin(QueryBuilder $queryBuilder, string $alias, string $association, string $originAlias = null): ?Join { @@ -71,4 +178,42 @@ private static function getExistingJoin(QueryBuilder $queryBuilder, string $alia return null; } + + /** + * Maps the root aliases to root entity classes. + * + * @return array + */ + private static function mapRootAliases(array $rootAliases, array $rootEntities): array + { + $aliasMap = array_combine($rootAliases, $rootEntities); + if (false === $aliasMap) { + throw new \LogicException('Number of root aliases and root entities do not match.'); + } + + return $aliasMap; + } + + /** + * Maps the join aliases to the parent alias and association, or the entity class. + * + * @return array + */ + private static function mapJoinAliases(iterable $joins): array + { + $aliasMap = []; + + foreach ($joins as $join) { + $alias = $join->getAlias(); + $relationship = $join->getJoin(); + + if (false !== strpos($relationship, '.')) { + $aliasMap[$alias] = explode('.', $relationship); + } else { + $aliasMap[$alias] = $relationship; + } + } + + return $aliasMap; + } } diff --git a/src/Bridge/Doctrine/Orm/Util/QueryChecker.php b/src/Bridge/Doctrine/Orm/Util/QueryChecker.php index 88eddd0cc95..ff1ed13c86b 100644 --- a/src/Bridge/Doctrine/Orm/Util/QueryChecker.php +++ b/src/Bridge/Doctrine/Orm/Util/QueryChecker.php @@ -23,6 +23,8 @@ * * @author Teoh Han Hui * @author Vincent Chalamon + * + * @internal */ final class QueryChecker { @@ -31,40 +33,44 @@ private function __construct() } /** - * Determines whether the query builder uses a HAVING clause. + * Determines whether the QueryBuilder uses a HAVING clause. */ public static function hasHavingClause(QueryBuilder $queryBuilder): bool { - return !empty($queryBuilder->getDQLPart('having')); + return null !== $queryBuilder->getDQLPart('having'); } /** - * Determines whether the query builder has any root entity with foreign key identifier. + * Determines whether the QueryBuilder has any root entity with foreign key identifier. */ public static function hasRootEntityWithForeignKeyIdentifier(QueryBuilder $queryBuilder, ManagerRegistry $managerRegistry): bool { - return self::hasRootEntityWithIdentifier($queryBuilder, $managerRegistry, true); - } + foreach ($queryBuilder->getRootEntities() as $rootEntity) { + /** @var ClassMetadata $rootMetadata */ + $rootMetadata = $managerRegistry + ->getManagerForClass($rootEntity) + ->getClassMetadata($rootEntity); - /** - * Determines whether the query builder has any composite identifier. - */ - public static function hasRootEntityWithCompositeIdentifier(QueryBuilder $queryBuilder, ManagerRegistry $managerRegistry): bool - { - return self::hasRootEntityWithIdentifier($queryBuilder, $managerRegistry, false); + if ($rootMetadata->containsForeignIdentifier) { + return true; + } + } + + return false; } /** - * Detects if the root entity has the given identifier. + * Determines whether the QueryBuilder has any composite identifier. */ - private static function hasRootEntityWithIdentifier(QueryBuilder $queryBuilder, ManagerRegistry $managerRegistry, bool $isForeign): bool + public static function hasRootEntityWithCompositeIdentifier(QueryBuilder $queryBuilder, ManagerRegistry $managerRegistry): bool { foreach ($queryBuilder->getRootEntities() as $rootEntity) { + /** @var ClassMetadata $rootMetadata */ $rootMetadata = $managerRegistry ->getManagerForClass($rootEntity) ->getClassMetadata($rootEntity); - if ($rootMetadata instanceof ClassMetadata && ($isForeign ? $rootMetadata->containsForeignIdentifier : $rootMetadata->isIdentifierComposite)) { + if ($rootMetadata->isIdentifierComposite) { return true; } } @@ -73,7 +79,7 @@ private static function hasRootEntityWithIdentifier(QueryBuilder $queryBuilder, } /** - * Determines whether the query builder has the maximum number of results specified. + * Determines whether the QueryBuilder has a limit on the maximum number of results. */ public static function hasMaxResults(QueryBuilder $queryBuilder): bool { @@ -81,71 +87,62 @@ public static function hasMaxResults(QueryBuilder $queryBuilder): bool } /** - * Determines whether the query builder has ORDER BY on entity joined through - * to-many association. + * Determines whether the QueryBuilder has ORDER BY on a column from a fetch joined to-many association. */ - public static function hasOrderByOnToManyJoin(QueryBuilder $queryBuilder, ManagerRegistry $managerRegistry): bool + public static function hasOrderByOnFetchJoinedToManyAssociation(QueryBuilder $queryBuilder, ManagerRegistry $managerRegistry): bool { if ( - empty($orderByParts = $queryBuilder->getDQLPart('orderBy')) || - empty($joinParts = $queryBuilder->getDQLPart('join')) + 0 === \count($selectParts = $queryBuilder->getDQLPart('select')) || + 0 === \count($queryBuilder->getDQLPart('join')) || + 0 === \count($orderByParts = $queryBuilder->getDQLPart('orderBy')) ) { return false; } + $rootAliases = $queryBuilder->getRootAliases(); + + $selectAliases = []; + + foreach ($selectParts as $select) { + foreach ($select->getParts() as $part) { + [$alias] = explode('.', $part); + + $selectAliases[] = $alias; + } + } + + $selectAliases = array_diff($selectAliases, $rootAliases); + if (0 === \count($selectAliases)) { + return false; + } + $orderByAliases = []; + foreach ($orderByParts as $orderBy) { - $parts = $orderBy->getParts(); + foreach ($orderBy->getParts() as $part) { + if (false !== strpos($part, '.')) { + [$alias] = explode('.', $part); - foreach ($parts as $part) { - $pos = strpos($part, '.'); - if (false !== $pos) { - $orderByAliases[substr($part, 0, $pos)] = true; + $orderByAliases[] = $alias; } } } - if (!$orderByAliases) { + $orderByAliases = array_diff($orderByAliases, $rootAliases); + if (0 === \count($orderByAliases)) { return false; } - foreach ($joinParts as $joins) { - foreach ($joins as $join) { - $alias = $join->getAlias(); + foreach ($orderByAliases as $orderByAlias) { + $inToManyContext = false; - if (!isset($orderByAliases[$alias])) { - continue; + foreach (QueryBuilderHelper::traverseJoins($orderByAlias, $queryBuilder, $managerRegistry) as $alias => [$metadata, $association]) { + if ($inToManyContext && \in_array($alias, $selectAliases, true)) { + return true; } - $relationship = $join->getJoin(); - - if (false !== strpos($relationship, '.')) { - /* - * We select the parent alias because it may differ from the origin alias given above - * @see https://github.com/api-platform/core/issues/1313 - */ - [$relationAlias, $association] = explode('.', $relationship); - $metadata = QueryJoinParser::getClassMetadataFromJoinAlias($relationAlias, $queryBuilder, $managerRegistry); - if ($metadata->isCollectionValuedAssociation($association)) { - return true; - } - } else { - $parentMetadata = $managerRegistry->getManagerForClass($relationship)->getClassMetadata($relationship); - - foreach ($queryBuilder->getRootEntities() as $rootEntity) { - $rootMetadata = $managerRegistry - ->getManagerForClass($rootEntity) - ->getClassMetadata($rootEntity); - - if (!$rootMetadata instanceof ClassMetadata) { - continue; - } - - foreach ($rootMetadata->getAssociationsByTargetClass($relationship) as $association => $mapping) { - if ($parentMetadata->isCollectionValuedAssociation($association)) { - return true; - } - } - } + + if (null !== $association && $metadata->isCollectionValuedAssociation($association)) { + $inToManyContext = true; } } } @@ -154,13 +151,52 @@ public static function hasOrderByOnToManyJoin(QueryBuilder $queryBuilder, Manage } /** - * Determines whether the query builder already has a left join. + * Determines whether the QueryBuilder has ORDER BY on a column from a fetch joined to-many association. + * + * @deprecated + */ + public static function hasOrderByOnToManyJoin(QueryBuilder $queryBuilder, ManagerRegistry $managerRegistry): bool + { + @trigger_error(sprintf('The use of "%s::hasOrderByOnToManyJoin()" is deprecated since 2.4 and will be removed in 3.0. Use "%1$s::hasOrderByOnFetchJoinedToManyAssociation()" instead.', __CLASS__), E_USER_DEPRECATED); + + return self::hasOrderByOnFetchJoinedToManyAssociation($queryBuilder, $managerRegistry); + } + + /** + * Determines whether the QueryBuilder already has a left join. */ public static function hasLeftJoin(QueryBuilder $queryBuilder): bool { - foreach ($queryBuilder->getDQLPart('join') as $dqlParts) { - foreach ($dqlParts as $dqlPart) { - if (Join::LEFT_JOIN === $dqlPart->getJoinType()) { + foreach ($queryBuilder->getDQLPart('join') as $joins) { + foreach ($joins as $join) { + if (Join::LEFT_JOIN === $join->getJoinType()) { + return true; + } + } + } + + return false; + } + + /** + * Determines whether the QueryBuilder has a joined to-many association. + */ + public static function hasJoinedToManyAssociation(QueryBuilder $queryBuilder, ManagerRegistry $managerRegistry): bool + { + if ( + 0 === \count($queryBuilder->getDQLPart('join')) + ) { + return false; + } + + $joinAliases = array_diff($queryBuilder->getAllAliases(), $queryBuilder->getRootAliases()); + if (0 === \count($joinAliases)) { + return false; + } + + foreach ($joinAliases as $joinAlias) { + foreach (QueryBuilderHelper::traverseJoins($joinAlias, $queryBuilder, $managerRegistry) as $alias => [$metadata, $association]) { + if (null !== $association && $metadata->isCollectionValuedAssociation($association)) { return true; } } diff --git a/src/Bridge/Doctrine/Orm/Util/QueryJoinParser.php b/src/Bridge/Doctrine/Orm/Util/QueryJoinParser.php index 478a96c004f..6d8ee127486 100644 --- a/src/Bridge/Doctrine/Orm/Util/QueryJoinParser.php +++ b/src/Bridge/Doctrine/Orm/Util/QueryJoinParser.php @@ -24,6 +24,10 @@ * * @author Teoh Han Hui * @author Vincent Chalamon + * + * @internal + * + * @deprecated */ final class QueryJoinParser { @@ -33,70 +37,24 @@ private function __construct() /** * Gets the class metadata from a given join alias. + * + * @deprecated */ public static function getClassMetadataFromJoinAlias(string $alias, QueryBuilder $queryBuilder, ManagerRegistry $managerRegistry): ClassMetadata { - $rootEntities = $queryBuilder->getRootEntities(); - $rootAliases = $queryBuilder->getRootAliases(); - - $joinParts = $queryBuilder->getDQLPart('join'); - - $aliasMap = []; - $targetAlias = $alias; - - foreach ($joinParts as $rootAlias => $joins) { - $aliasMap[$rootAlias] = 'root'; - - foreach ($joins as $join) { - $alias = $join->getAlias(); - $relationship = $join->getJoin(); - - $pos = strpos($relationship, '.'); - - if (false !== $pos) { - $aliasMap[$alias] = [ - 'parentAlias' => substr($relationship, 0, $pos), - 'association' => substr($relationship, $pos + 1), - ]; - } - } - } - - $associationStack = []; - $rootAlias = null; + @trigger_error(sprintf('The use of "%s::getClassMetadataFromJoinAlias()" is deprecated since 2.4 and will be removed in 3.0. Use "%s::getEntityClassByAlias()" instead.', __CLASS__, QueryBuilderHelper::class), E_USER_DEPRECATED); - while (null === $rootAlias) { - $mapping = $aliasMap[$targetAlias]; + $entityClass = QueryBuilderHelper::getEntityClassByAlias($alias, $queryBuilder, $managerRegistry); - if ('root' === $mapping) { - $rootAlias = $targetAlias; - } else { - $associationStack[] = $mapping['association']; - $targetAlias = $mapping['parentAlias']; - } - } - - $rootEntity = $rootEntities[array_search($rootAlias, $rootAliases, true)]; - - $rootMetadata = $managerRegistry - ->getManagerForClass($rootEntity) - ->getClassMetadata($rootEntity); - - $metadata = $rootMetadata; - - while (null !== ($association = array_pop($associationStack))) { - $associationClass = $metadata->getAssociationTargetClass($association); - - $metadata = $managerRegistry - ->getManagerForClass($associationClass) - ->getClassMetadata($associationClass); - } - - return $metadata; + return $managerRegistry + ->getManagerForClass($entityClass) + ->getClassMetadata($entityClass); } /** * Gets the relationship from a Join expression. + * + * @deprecated */ public static function getJoinRelationship(Join $join): string { @@ -107,6 +65,8 @@ public static function getJoinRelationship(Join $join): string /** * Gets the alias from a Join expression. + * + * @deprecated */ public static function getJoinAlias(Join $join): string { @@ -119,6 +79,8 @@ public static function getJoinAlias(Join $join): string * Gets the parts from an OrderBy expression. * * @return string[] + * + * @deprecated */ public static function getOrderByParts(OrderBy $orderBy): array { diff --git a/tests/Bridge/Doctrine/Orm/Extension/PaginationExtensionTest.php b/tests/Bridge/Doctrine/Orm/Extension/PaginationExtensionTest.php index 6381c4fdf82..25dacedaa17 100644 --- a/tests/Bridge/Doctrine/Orm/Extension/PaginationExtensionTest.php +++ b/tests/Bridge/Doctrine/Orm/Extension/PaginationExtensionTest.php @@ -13,6 +13,7 @@ namespace ApiPlatform\Core\Tests\Bridge\Doctrine\Orm\Extension; +use ApiPlatform\Core\Bridge\Doctrine\Orm\AbstractPaginator; use ApiPlatform\Core\Bridge\Doctrine\Orm\Extension\ContextAwareQueryResultCollectionExtensionInterface; use ApiPlatform\Core\Bridge\Doctrine\Orm\Extension\PaginationExtension; use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryNameGenerator; @@ -22,9 +23,11 @@ use ApiPlatform\Core\Exception\InvalidArgumentException; use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface; use ApiPlatform\Core\Metadata\Resource\ResourceMetadata; +use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\Dummy; use Doctrine\Common\Persistence\ManagerRegistry; use Doctrine\ORM\Configuration; use Doctrine\ORM\EntityManagerInterface; +use Doctrine\ORM\Mapping\ClassMetadata; use Doctrine\ORM\Query; use Doctrine\ORM\QueryBuilder; use Doctrine\ORM\Tools\Pagination\CountWalker; @@ -882,7 +885,34 @@ public function testLegacySupportsResultPaginationDisabled() public function testGetResult() { - $result = $this->getPaginationExtensionResult(); + $dummyMetadata = new ClassMetadata(Dummy::class); + + $entityManagerProphecy = $this->prophesize(EntityManagerInterface::class); + $entityManagerProphecy->getConfiguration()->willReturn(new Configuration()); + $entityManagerProphecy->getClassMetadata(Dummy::class)->willReturn($dummyMetadata); + + $queryBuilder = new QueryBuilder($entityManagerProphecy->reveal()); + $queryBuilder->select(['o']); + $queryBuilder->from(Dummy::class, 'o'); + $queryBuilder->setFirstResult(0); + $queryBuilder->setMaxResults(42); + + $query = new Query($entityManagerProphecy->reveal()); + $entityManagerProphecy->createQuery($queryBuilder->getDQL())->willReturn($query); + + $managerRegistryProphecy = $this->prophesize(ManagerRegistry::class); + $managerRegistryProphecy->getManagerForClass(Dummy::class)->willReturn($entityManagerProphecy); + + $resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataFactoryInterface::class); + $resourceMetadataFactoryProphecy->create(Dummy::class)->willReturn(new ResourceMetadata('Dummy')); + + $paginationExtension = new PaginationExtension( + $managerRegistryProphecy->reveal(), + $resourceMetadataFactoryProphecy->reveal(), + new Pagination($resourceMetadataFactoryProphecy->reveal()) + ); + + $result = $paginationExtension->getResult($queryBuilder, Dummy::class, 'get'); $this->assertInstanceOf(PartialPaginatorInterface::class, $result); $this->assertInstanceOf(PaginatorInterface::class, $result); @@ -890,26 +920,34 @@ public function testGetResult() public function testGetResultWithoutDistinct() { - $configuration = new Configuration(); + $dummyMetadata = new ClassMetadata(Dummy::class); $entityManagerProphecy = $this->prophesize(EntityManagerInterface::class); - $entityManagerProphecy->getConfiguration()->willReturn($configuration)->shouldBeCalled(); + $entityManagerProphecy->getConfiguration()->willReturn(new Configuration()); + $entityManagerProphecy->getClassMetadata(Dummy::class)->willReturn($dummyMetadata); + + $queryBuilder = new QueryBuilder($entityManagerProphecy->reveal()); + $queryBuilder->select(['o']); + $queryBuilder->from(Dummy::class, 'o'); + $queryBuilder->setFirstResult(0); + $queryBuilder->setMaxResults(42); $query = new Query($entityManagerProphecy->reveal()); - $query->setFirstResult(0); - $query->setMaxResults(42); + $entityManagerProphecy->createQuery($queryBuilder->getDQL())->willReturn($query); - $queryBuilderProphecy = $this->prophesize(QueryBuilder::class); - $queryBuilderProphecy->getRootEntities()->willReturn([])->shouldBeCalled(); - $queryBuilderProphecy->getAllAliases()->willReturn(['o'])->shouldBeCalled(); - $queryBuilderProphecy->getQuery()->willReturn($query)->shouldBeCalled(); - $queryBuilderProphecy->getDQLPart(Argument::that(function ($arg) { - return \in_array($arg, ['having', 'orderBy', 'join'], true); - }))->willReturn('')->shouldBeCalled(); - $queryBuilderProphecy->getMaxResults()->willReturn(42)->shouldBeCalled(); - $queryBuilder = $queryBuilderProphecy->reveal(); + $managerRegistryProphecy = $this->prophesize(ManagerRegistry::class); + $managerRegistryProphecy->getManagerForClass(Dummy::class)->willReturn($entityManagerProphecy); - $result = $this->getPaginationExtensionResult(false, false, true, $queryBuilder); + $resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataFactoryInterface::class); + $resourceMetadataFactoryProphecy->create(Dummy::class)->willReturn(new ResourceMetadata('Dummy')); + + $paginationExtension = new PaginationExtension( + $managerRegistryProphecy->reveal(), + $resourceMetadataFactoryProphecy->reveal(), + new Pagination($resourceMetadataFactoryProphecy->reveal()) + ); + + $result = $paginationExtension->getResult($queryBuilder, Dummy::class, 'get'); $this->assertInstanceOf(PartialPaginatorInterface::class, $result); $this->assertInstanceOf(PaginatorInterface::class, $result); @@ -924,18 +962,81 @@ public function testGetResultWithoutDistinct() */ public function testLegacyGetResult() { - $result = $this->getLegacyPaginationExtensionResult(); + $dummyMetadata = new ClassMetadata(Dummy::class); + + $entityManagerProphecy = $this->prophesize(EntityManagerInterface::class); + $entityManagerProphecy->getConfiguration()->willReturn(new Configuration()); + $entityManagerProphecy->getClassMetadata(Dummy::class)->willReturn($dummyMetadata); + + $queryBuilder = new QueryBuilder($entityManagerProphecy->reveal()); + $queryBuilder->select(['o']); + $queryBuilder->from(Dummy::class, 'o'); + $queryBuilder->setFirstResult(0); + $queryBuilder->setMaxResults(42); + + $query = new Query($entityManagerProphecy->reveal()); + $entityManagerProphecy->createQuery($queryBuilder->getDQL())->willReturn($query); + + $managerRegistryProphecy = $this->prophesize(ManagerRegistry::class); + $managerRegistryProphecy->getManagerForClass(Dummy::class)->willReturn($entityManagerProphecy); + + $requestStack = new RequestStack(); + $requestStack->push(new Request()); + + $resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataFactoryInterface::class); + $resourceMetadataFactoryProphecy->create(Dummy::class)->willReturn(new ResourceMetadata('Dummy')); + + $paginationExtension = new PaginationExtension( + $managerRegistryProphecy->reveal(), + $requestStack, + $resourceMetadataFactoryProphecy->reveal() + ); + + $result = $paginationExtension->getResult($queryBuilder, Dummy::class, 'get'); $this->assertInstanceOf(PartialPaginatorInterface::class, $result); $this->assertInstanceOf(PaginatorInterface::class, $result); } - public function testGetResultWithoutFetchJoinCollection() + public function testGetResultWithFetchJoinCollectionDisabled() { - $result = $this->getPaginationExtensionResult(false, false, false); + $dummyMetadata = new ClassMetadata(Dummy::class); + + $entityManagerProphecy = $this->prophesize(EntityManagerInterface::class); + $entityManagerProphecy->getConfiguration()->willReturn(new Configuration()); + $entityManagerProphecy->getClassMetadata(Dummy::class)->willReturn($dummyMetadata); + + $queryBuilder = new QueryBuilder($entityManagerProphecy->reveal()); + $queryBuilder->select(['o']); + $queryBuilder->from(Dummy::class, 'o'); + $queryBuilder->setFirstResult(0); + $queryBuilder->setMaxResults(42); + + $query = new Query($entityManagerProphecy->reveal()); + $entityManagerProphecy->createQuery($queryBuilder->getDQL())->willReturn($query); + + $managerRegistryProphecy = $this->prophesize(ManagerRegistry::class); + $managerRegistryProphecy->getManagerForClass(Dummy::class)->willReturn($entityManagerProphecy); + + $resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataFactoryInterface::class); + $resourceMetadataFactoryProphecy->create(Dummy::class)->willReturn(new ResourceMetadata('Dummy', null, null, null, null, ['pagination_fetch_join_collection' => false])); + + $paginationExtension = new PaginationExtension( + $managerRegistryProphecy->reveal(), + $resourceMetadataFactoryProphecy->reveal(), + new Pagination($resourceMetadataFactoryProphecy->reveal()) + ); + + $result = $paginationExtension->getResult($queryBuilder, Dummy::class, 'get'); $this->assertInstanceOf(PartialPaginatorInterface::class, $result); $this->assertInstanceOf(PaginatorInterface::class, $result); + + $doctrinePaginatorReflectionProperty = new \ReflectionProperty(AbstractPaginator::class, 'paginator'); + $doctrinePaginatorReflectionProperty->setAccessible(true); + + $doctrinePaginator = $doctrinePaginatorReflectionProperty->getValue($result); + $this->assertFalse($doctrinePaginator->getFetchJoinCollection()); } /** @@ -943,20 +1044,89 @@ public function testGetResultWithoutFetchJoinCollection() * @expectedDeprecation Passing an instance of "Symfony\Component\HttpFoundation\RequestStack" as second argument of "ApiPlatform\Core\Bridge\Doctrine\Orm\Extension\PaginationExtension" is deprecated since API Platform 2.4 and will not be possible anymore in API Platform 3. Pass an instance of "ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface" instead. * @expectedDeprecation Passing an instance of "ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface" as third argument of "ApiPlatform\Core\Bridge\Doctrine\Orm\Extension\PaginationExtension" is deprecated since API Platform 2.4 and will not be possible anymore in API Platform 3. Pass an instance of "ApiPlatform\Core\DataProvider\Pagination" instead. */ - public function testLegacyGetResultWithoutFetchJoinCollection() + public function testLegacyGetResultWithFetchJoinCollectionDisabled() { - $result = $this->getLegacyPaginationExtensionResult(false, false, false); + $dummyMetadata = new ClassMetadata(Dummy::class); + + $entityManagerProphecy = $this->prophesize(EntityManagerInterface::class); + $entityManagerProphecy->getConfiguration()->willReturn(new Configuration()); + $entityManagerProphecy->getClassMetadata(Dummy::class)->willReturn($dummyMetadata); + + $queryBuilder = new QueryBuilder($entityManagerProphecy->reveal()); + $queryBuilder->select(['o']); + $queryBuilder->from(Dummy::class, 'o'); + $queryBuilder->setFirstResult(0); + $queryBuilder->setMaxResults(42); + + $query = new Query($entityManagerProphecy->reveal()); + $entityManagerProphecy->createQuery($queryBuilder->getDQL())->willReturn($query); + + $managerRegistryProphecy = $this->prophesize(ManagerRegistry::class); + $managerRegistryProphecy->getManagerForClass(Dummy::class)->willReturn($entityManagerProphecy); + + $requestStack = new RequestStack(); + $requestStack->push(new Request()); + + $resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataFactoryInterface::class); + $resourceMetadataFactoryProphecy->create(Dummy::class)->willReturn(new ResourceMetadata('Dummy', null, null, null, null, ['pagination_fetch_join_collection' => false])); + + $paginationExtension = new PaginationExtension( + $managerRegistryProphecy->reveal(), + $requestStack, + $resourceMetadataFactoryProphecy->reveal() + ); + + $result = $paginationExtension->getResult($queryBuilder, Dummy::class, 'get'); $this->assertInstanceOf(PartialPaginatorInterface::class, $result); $this->assertInstanceOf(PaginatorInterface::class, $result); + + $doctrinePaginatorReflectionProperty = new \ReflectionProperty(AbstractPaginator::class, 'paginator'); + $doctrinePaginatorReflectionProperty->setAccessible(true); + + $doctrinePaginator = $doctrinePaginatorReflectionProperty->getValue($result); + $this->assertFalse($doctrinePaginator->getFetchJoinCollection()); } - public function testGetResultWithPartial() + public function testGetResultWithUseOutputWalkersDisabled() { - $result = $this->getPaginationExtensionResult(true); + $dummyMetadata = new ClassMetadata(Dummy::class); + + $entityManagerProphecy = $this->prophesize(EntityManagerInterface::class); + $entityManagerProphecy->getConfiguration()->willReturn(new Configuration()); + $entityManagerProphecy->getClassMetadata(Dummy::class)->willReturn($dummyMetadata); + + $queryBuilder = new QueryBuilder($entityManagerProphecy->reveal()); + $queryBuilder->select(['o']); + $queryBuilder->from(Dummy::class, 'o'); + $queryBuilder->setFirstResult(0); + $queryBuilder->setMaxResults(42); + + $query = new Query($entityManagerProphecy->reveal()); + $entityManagerProphecy->createQuery($queryBuilder->getDQL())->willReturn($query); + + $managerRegistryProphecy = $this->prophesize(ManagerRegistry::class); + $managerRegistryProphecy->getManagerForClass(Dummy::class)->willReturn($entityManagerProphecy); + + $resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataFactoryInterface::class); + $resourceMetadataFactoryProphecy->create(Dummy::class)->willReturn(new ResourceMetadata('Dummy', null, null, null, null, ['pagination_use_output_walkers' => false])); + + $paginationExtension = new PaginationExtension( + $managerRegistryProphecy->reveal(), + $resourceMetadataFactoryProphecy->reveal(), + new Pagination($resourceMetadataFactoryProphecy->reveal()) + ); + + $result = $paginationExtension->getResult($queryBuilder, Dummy::class, 'get'); $this->assertInstanceOf(PartialPaginatorInterface::class, $result); - $this->assertNotInstanceOf(PaginatorInterface::class, $result); + $this->assertInstanceOf(PaginatorInterface::class, $result); + + $doctrinePaginatorReflectionProperty = new \ReflectionProperty(AbstractPaginator::class, 'paginator'); + $doctrinePaginatorReflectionProperty->setAccessible(true); + + $doctrinePaginator = $doctrinePaginatorReflectionProperty->getValue($result); + $this->assertFalse($doctrinePaginator->getUseOutputWalkers()); } /** @@ -964,20 +1134,83 @@ public function testGetResultWithPartial() * @expectedDeprecation Passing an instance of "Symfony\Component\HttpFoundation\RequestStack" as second argument of "ApiPlatform\Core\Bridge\Doctrine\Orm\Extension\PaginationExtension" is deprecated since API Platform 2.4 and will not be possible anymore in API Platform 3. Pass an instance of "ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface" instead. * @expectedDeprecation Passing an instance of "ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface" as third argument of "ApiPlatform\Core\Bridge\Doctrine\Orm\Extension\PaginationExtension" is deprecated since API Platform 2.4 and will not be possible anymore in API Platform 3. Pass an instance of "ApiPlatform\Core\DataProvider\Pagination" instead. */ - public function testLegacyGetResultWithPartial() + public function testLegacyGetResultWithUseOutputWalkersDisabled() { - $result = $this->getLegacyPaginationExtensionResult(true); + $dummyMetadata = new ClassMetadata(Dummy::class); + + $entityManagerProphecy = $this->prophesize(EntityManagerInterface::class); + $entityManagerProphecy->getConfiguration()->willReturn(new Configuration()); + $entityManagerProphecy->getClassMetadata(Dummy::class)->willReturn($dummyMetadata); + + $queryBuilder = new QueryBuilder($entityManagerProphecy->reveal()); + $queryBuilder->select(['o']); + $queryBuilder->from(Dummy::class, 'o'); + $queryBuilder->setFirstResult(0); + $queryBuilder->setMaxResults(42); + + $query = new Query($entityManagerProphecy->reveal()); + $entityManagerProphecy->createQuery($queryBuilder->getDQL())->willReturn($query); + + $managerRegistryProphecy = $this->prophesize(ManagerRegistry::class); + $managerRegistryProphecy->getManagerForClass(Dummy::class)->willReturn($entityManagerProphecy); + + $requestStack = new RequestStack(); + $requestStack->push(new Request()); + + $resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataFactoryInterface::class); + $resourceMetadataFactoryProphecy->create(Dummy::class)->willReturn(new ResourceMetadata('Dummy', null, null, null, null, ['pagination_use_output_walkers' => false])); + + $paginationExtension = new PaginationExtension( + $managerRegistryProphecy->reveal(), + $requestStack, + $resourceMetadataFactoryProphecy->reveal() + ); + + $result = $paginationExtension->getResult($queryBuilder, Dummy::class, 'get'); $this->assertInstanceOf(PartialPaginatorInterface::class, $result); - $this->assertNotInstanceOf(PaginatorInterface::class, $result); + $this->assertInstanceOf(PaginatorInterface::class, $result); + + $doctrinePaginatorReflectionProperty = new \ReflectionProperty(AbstractPaginator::class, 'paginator'); + $doctrinePaginatorReflectionProperty->setAccessible(true); + + $doctrinePaginator = $doctrinePaginatorReflectionProperty->getValue($result); + $this->assertFalse($doctrinePaginator->getUseOutputWalkers()); } - public function testSimpleGetResult() + public function testGetResultWithPartial() { - $result = $this->getPaginationExtensionResult(false, true); + $dummyMetadata = new ClassMetadata(Dummy::class); + + $entityManagerProphecy = $this->prophesize(EntityManagerInterface::class); + $entityManagerProphecy->getConfiguration()->willReturn(new Configuration()); + $entityManagerProphecy->getClassMetadata(Dummy::class)->willReturn($dummyMetadata); + + $queryBuilder = new QueryBuilder($entityManagerProphecy->reveal()); + $queryBuilder->select(['o']); + $queryBuilder->from(Dummy::class, 'o'); + $queryBuilder->setFirstResult(0); + $queryBuilder->setMaxResults(42); + + $query = new Query($entityManagerProphecy->reveal()); + $entityManagerProphecy->createQuery($queryBuilder->getDQL())->willReturn($query); + + $managerRegistryProphecy = $this->prophesize(ManagerRegistry::class); + $managerRegistryProphecy->getManagerForClass(Dummy::class)->willReturn($entityManagerProphecy); + + $resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataFactoryInterface::class); + $resourceMetadataFactoryProphecy->create(Dummy::class)->willReturn(new ResourceMetadata('Dummy', null, null, null, null, ['pagination_partial' => true])); + + $paginationExtension = new PaginationExtension( + $managerRegistryProphecy->reveal(), + $resourceMetadataFactoryProphecy->reveal(), + new Pagination($resourceMetadataFactoryProphecy->reveal()) + ); + + $result = $paginationExtension->getResult($queryBuilder, Dummy::class, 'get'); $this->assertInstanceOf(PartialPaginatorInterface::class, $result); - $this->assertInstanceOf(PaginatorInterface::class, $result); + $this->assertNotInstanceOf(PaginatorInterface::class, $result); } /** @@ -985,104 +1218,117 @@ public function testSimpleGetResult() * @expectedDeprecation Passing an instance of "Symfony\Component\HttpFoundation\RequestStack" as second argument of "ApiPlatform\Core\Bridge\Doctrine\Orm\Extension\PaginationExtension" is deprecated since API Platform 2.4 and will not be possible anymore in API Platform 3. Pass an instance of "ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface" instead. * @expectedDeprecation Passing an instance of "ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface" as third argument of "ApiPlatform\Core\Bridge\Doctrine\Orm\Extension\PaginationExtension" is deprecated since API Platform 2.4 and will not be possible anymore in API Platform 3. Pass an instance of "ApiPlatform\Core\DataProvider\Pagination" instead. */ - public function testLegacySimpleGetResult() + public function testLegacyGetResultWithPartial() { - $result = $this->getLegacyPaginationExtensionResult(false, true); + $dummyMetadata = new ClassMetadata(Dummy::class); - $this->assertInstanceOf(PartialPaginatorInterface::class, $result); - $this->assertInstanceOf(PaginatorInterface::class, $result); - } + $entityManagerProphecy = $this->prophesize(EntityManagerInterface::class); + $entityManagerProphecy->getConfiguration()->willReturn(new Configuration()); + $entityManagerProphecy->getClassMetadata(Dummy::class)->willReturn($dummyMetadata); - private function getPaginationExtensionResult(bool $partial = false, bool $legacy = false, bool $fetchJoinCollection = true, QueryBuilder $queryBuilder = null) - { - $resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataFactoryInterface::class); - $resourceMetadataFactory = $resourceMetadataFactoryProphecy->reveal(); + $queryBuilder = new QueryBuilder($entityManagerProphecy->reveal()); + $queryBuilder->select(['o']); + $queryBuilder->from(Dummy::class, 'o'); + $queryBuilder->setFirstResult(0); + $queryBuilder->setMaxResults(42); - if (!$legacy) { - $resourceMetadataFactoryProphecy->create('Foo')->willReturn(new ResourceMetadata(null, null, null, [], [], ['pagination_partial' => false, 'pagination_client_partial' => true, 'pagination_fetch_join_collection' => $fetchJoinCollection])); - } + $query = new Query($entityManagerProphecy->reveal()); + $entityManagerProphecy->createQuery($queryBuilder->getDQL())->willReturn($query); - $pagination = new Pagination($resourceMetadataFactory); + $managerRegistryProphecy = $this->prophesize(ManagerRegistry::class); + $managerRegistryProphecy->getManagerForClass(Dummy::class)->willReturn($entityManagerProphecy); - $configuration = new Configuration(); + $requestStack = new RequestStack(); + $requestStack->push(new Request()); - $entityManagerProphecy = $this->prophesize(EntityManagerInterface::class); - $entityManagerProphecy->getConfiguration()->willReturn($configuration); - - if (null === $queryBuilder) { - $query = new Query($entityManagerProphecy->reveal()); - $query->setFirstResult(0); - $query->setMaxResults(42); - - $queryBuilderProphecy = $this->prophesize(QueryBuilder::class); - $queryBuilderProphecy->getRootEntities()->willReturn([])->shouldBeCalled(); - $queryBuilderProphecy->getAllAliases()->willReturn([])->shouldBeCalled(); - $queryBuilderProphecy->getQuery()->willReturn($query)->shouldBeCalled(); - $queryBuilderProphecy->getDQLPart(Argument::that(function ($arg) { - return \in_array($arg, ['having', 'orderBy', 'join'], true); - }))->willReturn('')->shouldBeCalled(); - $queryBuilderProphecy->getMaxResults()->willReturn(42)->shouldBeCalled(); - $queryBuilder = $queryBuilderProphecy->reveal(); - } + $resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataFactoryInterface::class); + $resourceMetadataFactoryProphecy->create(Dummy::class)->willReturn(new ResourceMetadata('Dummy', null, null, null, null, ['pagination_partial' => true])); $paginationExtension = new PaginationExtension( - $this->prophesize(ManagerRegistry::class)->reveal(), - $resourceMetadataFactory, - $pagination + $managerRegistryProphecy->reveal(), + $requestStack, + $resourceMetadataFactoryProphecy->reveal() ); - $args = [$queryBuilder, null, null, ['filters' => ['partial' => $partial]]]; + $result = $paginationExtension->getResult($queryBuilder, Dummy::class, 'get'); - if (!$legacy) { - $args[1] = 'Foo'; - $args[2] = null; - } - - return $paginationExtension->getResult(...$args); + $this->assertInstanceOf(PartialPaginatorInterface::class, $result); + $this->assertNotInstanceOf(PaginatorInterface::class, $result); } - private function getLegacyPaginationExtensionResult(bool $partial = false, bool $legacy = false, bool $fetchJoinCollection = true) + public function testSimpleGetResult() { - $requestStack = new RequestStack(); - $requestStack->push(new Request(['partial' => $partial])); + $dummyMetadata = new ClassMetadata(Dummy::class); + + $entityManagerProphecy = $this->prophesize(EntityManagerInterface::class); + $entityManagerProphecy->getConfiguration()->willReturn(new Configuration()); + $entityManagerProphecy->getClassMetadata(Dummy::class)->willReturn($dummyMetadata); + + $queryBuilder = new QueryBuilder($entityManagerProphecy->reveal()); + $queryBuilder->select(['o']); + $queryBuilder->from(Dummy::class, 'o'); + $queryBuilder->setFirstResult(0); + $queryBuilder->setMaxResults(42); + + $query = new Query($entityManagerProphecy->reveal()); + $entityManagerProphecy->createQuery($queryBuilder->getDQL())->willReturn($query); + + $managerRegistryProphecy = $this->prophesize(ManagerRegistry::class); + $managerRegistryProphecy->getManagerForClass(Dummy::class)->willReturn($entityManagerProphecy); $resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataFactoryInterface::class); - if (!$legacy) { - $resourceMetadataFactoryProphecy->create('Foo')->willReturn(new ResourceMetadata(null, null, null, [], [], ['pagination_partial' => false, 'pagination_client_partial' => true, 'pagination_fetch_join_collection' => $fetchJoinCollection]))->shouldBeCalled(); - } + $paginationExtension = new PaginationExtension( + $managerRegistryProphecy->reveal(), + $resourceMetadataFactoryProphecy->reveal(), + new Pagination($resourceMetadataFactoryProphecy->reveal()) + ); - $configuration = new Configuration(); + $result = $paginationExtension->getResult($queryBuilder); + + $this->assertInstanceOf(PartialPaginatorInterface::class, $result); + $this->assertInstanceOf(PaginatorInterface::class, $result); + } + + /** + * @group legacy + * @expectedDeprecation Passing an instance of "Symfony\Component\HttpFoundation\RequestStack" as second argument of "ApiPlatform\Core\Bridge\Doctrine\Orm\Extension\PaginationExtension" is deprecated since API Platform 2.4 and will not be possible anymore in API Platform 3. Pass an instance of "ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface" instead. + * @expectedDeprecation Passing an instance of "ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface" as third argument of "ApiPlatform\Core\Bridge\Doctrine\Orm\Extension\PaginationExtension" is deprecated since API Platform 2.4 and will not be possible anymore in API Platform 3. Pass an instance of "ApiPlatform\Core\DataProvider\Pagination" instead. + */ + public function testLegacySimpleGetResult() + { + $dummyMetadata = new ClassMetadata(Dummy::class); $entityManagerProphecy = $this->prophesize(EntityManagerInterface::class); - $entityManagerProphecy->getConfiguration()->willReturn($configuration)->shouldBeCalled(); + $entityManagerProphecy->getConfiguration()->willReturn(new Configuration()); + $entityManagerProphecy->getClassMetadata(Dummy::class)->willReturn($dummyMetadata); + + $queryBuilder = new QueryBuilder($entityManagerProphecy->reveal()); + $queryBuilder->select(['o']); + $queryBuilder->from(Dummy::class, 'o'); + $queryBuilder->setFirstResult(0); + $queryBuilder->setMaxResults(42); $query = new Query($entityManagerProphecy->reveal()); - $query->setFirstResult(0); - $query->setMaxResults(42); + $entityManagerProphecy->createQuery($queryBuilder->getDQL())->willReturn($query); - $queryBuilderProphecy = $this->prophesize(QueryBuilder::class); - $queryBuilderProphecy->getRootEntities()->willReturn([])->shouldBeCalled(); - $queryBuilderProphecy->getAllAliases()->willReturn([])->shouldBeCalled(); - $queryBuilderProphecy->getQuery()->willReturn($query)->shouldBeCalled(); - $queryBuilderProphecy->getDQLPart(Argument::that(function ($arg) { - return \in_array($arg, ['having', 'orderBy', 'join'], true); - }))->willReturn('')->shouldBeCalled(); - $queryBuilderProphecy->getMaxResults()->willReturn(42)->shouldBeCalled(); + $managerRegistryProphecy = $this->prophesize(ManagerRegistry::class); + $managerRegistryProphecy->getManagerForClass(Dummy::class)->willReturn($entityManagerProphecy); + + $requestStack = new RequestStack(); + $requestStack->push(new Request()); + + $resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataFactoryInterface::class); $paginationExtension = new PaginationExtension( - $this->prophesize(ManagerRegistry::class)->reveal(), + $managerRegistryProphecy->reveal(), $requestStack, $resourceMetadataFactoryProphecy->reveal() ); - $args = [$queryBuilderProphecy->reveal()]; - - if (!$legacy) { - $args[] = 'Foo'; - $args[] = null; - } + $result = $paginationExtension->getResult($queryBuilder); - return $paginationExtension->getResult(...$args); + $this->assertInstanceOf(PartialPaginatorInterface::class, $result); + $this->assertInstanceOf(PaginatorInterface::class, $result); } } diff --git a/tests/Bridge/Doctrine/Orm/Util/QueryBuilderHelperTest.php b/tests/Bridge/Doctrine/Orm/Util/QueryBuilderHelperTest.php index 6274eed4c7c..4161c5bc6fb 100644 --- a/tests/Bridge/Doctrine/Orm/Util/QueryBuilderHelperTest.php +++ b/tests/Bridge/Doctrine/Orm/Util/QueryBuilderHelperTest.php @@ -15,7 +15,11 @@ use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryBuilderHelper; use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryNameGeneratorInterface; +use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\Dummy; +use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\RelatedDummy; +use Doctrine\Common\Persistence\ManagerRegistry; use Doctrine\ORM\EntityManagerInterface; +use Doctrine\ORM\Mapping\ClassMetadata; use Doctrine\ORM\QueryBuilder; use PHPUnit\Framework\TestCase; @@ -49,6 +53,52 @@ public function testAddJoinOnce(?string $originAliasForJoinOnce, string $expecte $queryBuilder->getDQLPart('join')[$originAliasForJoinOnce ?? 'f'][0]->getAlias()); } + public function testGetEntityClassByAliasWithJoinByAssociation(): void + { + $dummyMetadata = new ClassMetadata(Dummy::class); + $dummyMetadata->mapManyToMany([ + 'fieldName' => 'relatedDummies', + 'targetEntity' => RelatedDummy::class, + ]); + + $relatedDummyMetadata = new ClassMetadata(RelatedDummy::class); + + $entityManagerProphecy = $this->prophesize(EntityManagerInterface::class); + $entityManagerProphecy->getClassMetadata(Dummy::class)->willReturn($dummyMetadata); + $entityManagerProphecy->getClassMetadata(RelatedDummy::class)->willReturn($relatedDummyMetadata); + + $queryBuilder = new QueryBuilder($entityManagerProphecy->reveal()); + $queryBuilder->from(Dummy::class, 'd'); + $queryBuilder->innerJoin('d.relatedDummies', 'a_1'); + + $managerRegistryProphecy = $this->prophesize(ManagerRegistry::class); + $managerRegistryProphecy->getManagerForClass(Dummy::class)->willReturn($entityManagerProphecy); + $managerRegistryProphecy->getManagerForClass(RelatedDummy::class)->willReturn($entityManagerProphecy); + + $actual = QueryBuilderHelper::getEntityClassByAlias('a_1', $queryBuilder, $managerRegistryProphecy->reveal()); + + $this->assertEquals(RelatedDummy::class, $actual); + } + + public function testGetEntityClassByAliasWithJoinByClass(): void + { + $relatedDummyMetadata = new ClassMetadata(RelatedDummy::class); + + $entityManagerProphecy = $this->prophesize(EntityManagerInterface::class); + $entityManagerProphecy->getClassMetadata(RelatedDummy::class)->willReturn($relatedDummyMetadata); + + $queryBuilder = new QueryBuilder($entityManagerProphecy->reveal()); + $queryBuilder->from(Dummy::class, 'd'); + $queryBuilder->innerJoin(RelatedDummy::class, 'a_1', null, 'd.name = a_1.name'); + + $managerRegistryProphecy = $this->prophesize(ManagerRegistry::class); + $managerRegistryProphecy->getManagerForClass(RelatedDummy::class)->willReturn($entityManagerProphecy); + + $actual = QueryBuilderHelper::getEntityClassByAlias('a_1', $queryBuilder, $managerRegistryProphecy->reveal()); + + $this->assertEquals(RelatedDummy::class, $actual); + } + public function provideAddJoinOnce(): array { return [ diff --git a/tests/Bridge/Doctrine/Orm/Util/QueryCheckerTest.php b/tests/Bridge/Doctrine/Orm/Util/QueryCheckerTest.php index 147dba1dba1..62ea581e9b3 100644 --- a/tests/Bridge/Doctrine/Orm/Util/QueryCheckerTest.php +++ b/tests/Bridge/Doctrine/Orm/Util/QueryCheckerTest.php @@ -14,12 +14,12 @@ namespace ApiPlatform\Core\Tests\Bridge\Doctrine\Orm\Util; use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryChecker; +use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\Dummy; use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\RelatedDummy; use Doctrine\Common\Persistence\ManagerRegistry; use Doctrine\Common\Persistence\ObjectManager; +use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\Mapping\ClassMetadata; -use Doctrine\ORM\Query\Expr\Join; -use Doctrine\ORM\Query\Expr\OrderBy; use Doctrine\ORM\QueryBuilder; use PHPUnit\Framework\TestCase; @@ -32,10 +32,10 @@ public function testHasHavingClauseWithHavingClause() $this->assertTrue(QueryChecker::hasHavingClause($queryBuilder->reveal())); } - public function testHasHavingClauseWithEmptyHavingClause() + public function testHasHavingClauseWithoutHavingClause() { $queryBuilder = $this->prophesize(QueryBuilder::class); - $queryBuilder->getDQLPart('having')->willReturn([]); + $queryBuilder->getDQLPart('having')->willReturn(null); $this->assertFalse(QueryChecker::hasHavingClause($queryBuilder->reveal())); } @@ -114,92 +114,232 @@ public function testHasRootEntityWithNoForeignKeyIdentifier() $this->assertFalse(QueryChecker::hasRootEntityWithForeignKeyIdentifier($queryBuilder->reveal(), $managerRegistry->reveal())); } - public function testHasOrderByOnToManyJoinWithoutJoin() + public function testHasOrderByOnFetchJoinedToManyAssociationWithoutJoin() { - $queryBuilder = $this->prophesize(QueryBuilder::class); - $queryBuilder->getRootEntities()->willReturn(['Dummy']); - $queryBuilder->getRootAliases()->willReturn(['d']); - $queryBuilder->getDQLPart('join')->willReturn([]); - $queryBuilder->getDQLPart('orderBy')->willReturn(['name' => new OrderBy('name', 'asc')]); - $classMetadata = $this->prophesize(ClassMetadata::class); - $objectManager = $this->prophesize(ObjectManager::class); - $objectManager->getClassMetadata('Dummy')->willReturn($classMetadata->reveal()); - $managerRegistry = $this->prophesize(ManagerRegistry::class); - $managerRegistry->getManagerForClass('Dummy')->willReturn($objectManager->reveal()); + $entityManagerProphecy = $this->prophesize(EntityManagerInterface::class); + + $queryBuilder = new QueryBuilder($entityManagerProphecy->reveal()); + $queryBuilder->select(['d']); + $queryBuilder->from(Dummy::class, 'd'); + $queryBuilder->orderBy('d.name', 'ASC'); + + $managerRegistryProphecy = $this->prophesize(ManagerRegistry::class); - $this->assertFalse(QueryChecker::hasOrderByOnToManyJoin($queryBuilder->reveal(), $managerRegistry->reveal())); + $this->assertFalse(QueryChecker::hasOrderByOnFetchJoinedToManyAssociation($queryBuilder, $managerRegistryProphecy->reveal())); } - public function testHasOrderByOnToManyJoinWithoutOrderBy() + public function testHasOrderByOnFetchJoinedToManyAssociationWithoutOrderBy() { - $queryBuilder = $this->prophesize(QueryBuilder::class); - $queryBuilder->getRootEntities()->willReturn(['Dummy']); - $queryBuilder->getRootAliases()->willReturn(['d']); - $queryBuilder->getDQLPart('join')->willReturn(['a_1' => new Join('INNER_JOIN', 'relatedDummy', 'a_1', null, 'a_1.name = r.name')]); - $queryBuilder->getDQLPart('orderBy')->willReturn([]); - $classMetadata = $this->prophesize(ClassMetadata::class); - $objectManager = $this->prophesize(ObjectManager::class); - $objectManager->getClassMetadata('Dummy')->willReturn($classMetadata->reveal()); - $managerRegistry = $this->prophesize(ManagerRegistry::class); - $managerRegistry->getManagerForClass('Dummy')->willReturn($objectManager->reveal()); + $entityManagerProphecy = $this->prophesize(EntityManagerInterface::class); + + $queryBuilder = new QueryBuilder($entityManagerProphecy->reveal()); + $queryBuilder->select(['d', 'a_1']); + $queryBuilder->from(Dummy::class, 'd'); + $queryBuilder->leftJoin('d.relatedDummies', 'a_1'); + + $managerRegistryProphecy = $this->prophesize(ManagerRegistry::class); - $this->assertFalse(QueryChecker::hasOrderByOnToManyJoin($queryBuilder->reveal(), $managerRegistry->reveal())); + $this->assertFalse(QueryChecker::hasOrderByOnFetchJoinedToManyAssociation($queryBuilder, $managerRegistryProphecy->reveal())); } - public function testHasOrderByOnToManyJoinWithoutJoinAndWithoutOrderBy() + public function testHasOrderByOnFetchJoinedToManyAssociationNotFetchJoined() { - $queryBuilder = $this->prophesize(QueryBuilder::class); - $queryBuilder->getRootEntities()->willReturn(['Dummy']); - $queryBuilder->getRootAliases()->willReturn(['d']); - $queryBuilder->getDQLPart('join')->willReturn([]); - $queryBuilder->getDQLPart('orderBy')->willReturn([]); - $classMetadata = $this->prophesize(ClassMetadata::class); - $objectManager = $this->prophesize(ObjectManager::class); - $objectManager->getClassMetadata('Dummy')->willReturn($classMetadata->reveal()); - $managerRegistry = $this->prophesize(ManagerRegistry::class); - $managerRegistry->getManagerForClass('Dummy')->willReturn($objectManager->reveal()); + $dummyMetadata = new ClassMetadata(Dummy::class); + $dummyMetadata->mapManyToMany([ + 'fieldName' => 'relatedDummies', + 'targetEntity' => RelatedDummy::class, + ]); + $dummyMetadata->mapManyToOne([ + 'fieldName' => 'relatedDummy', + 'targetEntity' => RelatedDummy::class, + ]); + + $relatedDummyMetadata = new ClassMetadata(RelatedDummy::class); + + $entityManagerProphecy = $this->prophesize(EntityManagerInterface::class); + $entityManagerProphecy->getClassMetadata(Dummy::class)->willReturn($dummyMetadata); + $entityManagerProphecy->getClassMetadata(RelatedDummy::class)->willReturn($relatedDummyMetadata); - $this->assertFalse(QueryChecker::hasOrderByOnToManyJoin($queryBuilder->reveal(), $managerRegistry->reveal())); + $queryBuilder = new QueryBuilder($entityManagerProphecy->reveal()); + $queryBuilder->select(['d', 'a_2']); + $queryBuilder->from(Dummy::class, 'd'); + $queryBuilder->leftJoin('d.relatedDummies', 'a_1'); + $queryBuilder->leftJoin('d.relatedDummy', 'a_2'); + $queryBuilder->orderBy('a_1.name', 'ASC'); + + $managerRegistryProphecy = $this->prophesize(ManagerRegistry::class); + $managerRegistryProphecy->getManagerForClass(Dummy::class)->willReturn($entityManagerProphecy); + $managerRegistryProphecy->getManagerForClass(RelatedDummy::class)->willReturn($entityManagerProphecy); + + $this->assertFalse(QueryChecker::hasOrderByOnFetchJoinedToManyAssociation($queryBuilder, $managerRegistryProphecy->reveal())); } - public function testHasOrderByOnToManyJoinWithClassLeftJoin() + public function testHasOrderByOnFetchJoinedToManyAssociationWithJoinByAssociation() { - $queryBuilder = $this->prophesize(QueryBuilder::class); - $queryBuilder->getRootEntities()->willReturn(['Dummy']); - $queryBuilder->getRootAliases()->willReturn(['d']); - $queryBuilder->getDQLPart('join')->willReturn(['a_1' => [new Join('LEFT_JOIN', RelatedDummy::class, 'a_1', null, 'a_1.name = d.name')]]); - $queryBuilder->getDQLPart('orderBy')->willReturn(['a_1.name' => new OrderBy('a_1.name', 'asc')]); - $classMetadata = $this->prophesize(ClassMetadata::class); - $classMetadata->getAssociationsByTargetClass(RelatedDummy::class)->willReturn(['relatedDummy' => ['targetEntity' => RelatedDummy::class]]); - $relatedClassMetadata = $this->prophesize(ClassMetadata::class); - $relatedClassMetadata->isCollectionValuedAssociation('relatedDummy')->willReturn(true); - $objectManager = $this->prophesize(ObjectManager::class); - $objectManager->getClassMetadata('Dummy')->willReturn($classMetadata->reveal()); - $objectManager->getClassMetadata(RelatedDummy::class)->willReturn($relatedClassMetadata->reveal()); - $managerRegistry = $this->prophesize(ManagerRegistry::class); - $managerRegistry->getManagerForClass('Dummy')->willReturn($objectManager->reveal()); - $managerRegistry->getManagerForClass(RelatedDummy::class)->willReturn($objectManager->reveal()); + $dummyMetadata = new ClassMetadata(Dummy::class); + $dummyMetadata->mapManyToMany([ + 'fieldName' => 'relatedDummies', + 'targetEntity' => RelatedDummy::class, + ]); - $this->assertTrue(QueryChecker::hasOrderByOnToManyJoin($queryBuilder->reveal(), $managerRegistry->reveal())); + $relatedDummyMetadata = new ClassMetadata(RelatedDummy::class); + + $entityManagerProphecy = $this->prophesize(EntityManagerInterface::class); + $entityManagerProphecy->getClassMetadata(Dummy::class)->willReturn($dummyMetadata); + $entityManagerProphecy->getClassMetadata(RelatedDummy::class)->willReturn($relatedDummyMetadata); + + $queryBuilder = new QueryBuilder($entityManagerProphecy->reveal()); + $queryBuilder->select(['d', 'a_1']); + $queryBuilder->from(Dummy::class, 'd'); + $queryBuilder->leftJoin('d.relatedDummies', 'a_1'); + $queryBuilder->orderBy('a_1.name', 'ASC'); + + $managerRegistryProphecy = $this->prophesize(ManagerRegistry::class); + $managerRegistryProphecy->getManagerForClass(Dummy::class)->willReturn($entityManagerProphecy); + $managerRegistryProphecy->getManagerForClass(RelatedDummy::class)->willReturn($entityManagerProphecy); + + $this->assertTrue(QueryChecker::hasOrderByOnFetchJoinedToManyAssociation($queryBuilder, $managerRegistryProphecy->reveal())); } /** - * Adds a test on the fix referenced in https://github.com/api-platform/core/pull/1449. + * @group legacy + * @expectedDeprecation The use of "ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryChecker::hasOrderByOnToManyJoin()" is deprecated since 2.4 and will be removed in 3.0. Use "ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryChecker::hasOrderByOnFetchJoinedToManyAssociation()" instead. */ - public function testOrderByOnToManyWithRelationAsBasis() + public function testHasOrderByOnToManyJoinWithoutJoin() { - $queryBuilder = $this->prophesize(QueryBuilder::class); - $queryBuilder->getRootEntities()->willReturn(['Dummy']); - $queryBuilder->getRootAliases()->willReturn(['d']); - $queryBuilder->getDQLPart('join')->willReturn(['d' => [new Join('LEFT_JOIN', 'd.relatedDummy', 'a_1')]]); - $queryBuilder->getDQLPart('orderBy')->willReturn(['a_1.name' => new OrderBy('a_1.name', 'asc')]); - $classMetadata = $this->prophesize(ClassMetadata::class); - $classMetadata->isCollectionValuedAssociation('relatedDummy')->willReturn(true); - $objectManager = $this->prophesize(ObjectManager::class); - $objectManager->getClassMetadata('Dummy')->willReturn($classMetadata->reveal()); - $managerRegistry = $this->prophesize(ManagerRegistry::class); - $managerRegistry->getManagerForClass('Dummy')->willReturn($objectManager->reveal()); + $entityManagerProphecy = $this->prophesize(EntityManagerInterface::class); + + $queryBuilder = new QueryBuilder($entityManagerProphecy->reveal()); + $queryBuilder->select(['d']); + $queryBuilder->from(Dummy::class, 'd'); + $queryBuilder->orderBy('d.name', 'ASC'); + + $managerRegistryProphecy = $this->prophesize(ManagerRegistry::class); + + $this->assertFalse(QueryChecker::hasOrderByOnToManyJoin($queryBuilder, $managerRegistryProphecy->reveal())); + } + + /** + * @group legacy + * @expectedDeprecation The use of "ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryChecker::hasOrderByOnToManyJoin()" is deprecated since 2.4 and will be removed in 3.0. Use "ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryChecker::hasOrderByOnFetchJoinedToManyAssociation()" instead. + */ + public function testHasOrderByOnToManyJoinWithoutOrderBy() + { + $entityManagerProphecy = $this->prophesize(EntityManagerInterface::class); + + $queryBuilder = new QueryBuilder($entityManagerProphecy->reveal()); + $queryBuilder->select(['d', 'a_1']); + $queryBuilder->from(Dummy::class, 'd'); + $queryBuilder->leftJoin('d.relatedDummies', 'a_1'); + + $managerRegistryProphecy = $this->prophesize(ManagerRegistry::class); + + $this->assertFalse(QueryChecker::hasOrderByOnToManyJoin($queryBuilder, $managerRegistryProphecy->reveal())); + } + + /** + * @group legacy + * @expectedDeprecation The use of "ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryChecker::hasOrderByOnToManyJoin()" is deprecated since 2.4 and will be removed in 3.0. Use "ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryChecker::hasOrderByOnFetchJoinedToManyAssociation()" instead. + */ + public function testHasOrderByOnToManyJoinNotFetchJoined() + { + $dummyMetadata = new ClassMetadata(Dummy::class); + $dummyMetadata->mapManyToMany([ + 'fieldName' => 'relatedDummies', + 'targetEntity' => RelatedDummy::class, + ]); + $dummyMetadata->mapManyToOne([ + 'fieldName' => 'relatedDummy', + 'targetEntity' => RelatedDummy::class, + ]); + + $relatedDummyMetadata = new ClassMetadata(RelatedDummy::class); + + $entityManagerProphecy = $this->prophesize(EntityManagerInterface::class); + $entityManagerProphecy->getClassMetadata(Dummy::class)->willReturn($dummyMetadata); + $entityManagerProphecy->getClassMetadata(RelatedDummy::class)->willReturn($relatedDummyMetadata); + + $queryBuilder = new QueryBuilder($entityManagerProphecy->reveal()); + $queryBuilder->select(['d', 'a_2']); + $queryBuilder->from(Dummy::class, 'd'); + $queryBuilder->leftJoin('d.relatedDummies', 'a_1'); + $queryBuilder->leftJoin('d.relatedDummy', 'a_2'); + $queryBuilder->orderBy('a_1.name', 'ASC'); + + $managerRegistryProphecy = $this->prophesize(ManagerRegistry::class); + $managerRegistryProphecy->getManagerForClass(Dummy::class)->willReturn($entityManagerProphecy); + $managerRegistryProphecy->getManagerForClass(RelatedDummy::class)->willReturn($entityManagerProphecy); + + $this->assertFalse(QueryChecker::hasOrderByOnToManyJoin($queryBuilder, $managerRegistryProphecy->reveal())); + } + + /** + * @group legacy + * @expectedDeprecation The use of "ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryChecker::hasOrderByOnToManyJoin()" is deprecated since 2.4 and will be removed in 3.0. Use "ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryChecker::hasOrderByOnFetchJoinedToManyAssociation()" instead. + */ + public function testHasOrderByOnToManyWithJoinByAssociation() + { + $dummyMetadata = new ClassMetadata(Dummy::class); + $dummyMetadata->mapManyToMany([ + 'fieldName' => 'relatedDummies', + 'targetEntity' => RelatedDummy::class, + ]); + + $relatedDummyMetadata = new ClassMetadata(RelatedDummy::class); + + $entityManagerProphecy = $this->prophesize(EntityManagerInterface::class); + $entityManagerProphecy->getClassMetadata(Dummy::class)->willReturn($dummyMetadata); + $entityManagerProphecy->getClassMetadata(RelatedDummy::class)->willReturn($relatedDummyMetadata); + + $queryBuilder = new QueryBuilder($entityManagerProphecy->reveal()); + $queryBuilder->select(['d', 'a_1']); + $queryBuilder->from(Dummy::class, 'd'); + $queryBuilder->leftJoin('d.relatedDummies', 'a_1'); + $queryBuilder->orderBy('a_1.name', 'ASC'); + + $managerRegistryProphecy = $this->prophesize(ManagerRegistry::class); + $managerRegistryProphecy->getManagerForClass(Dummy::class)->willReturn($entityManagerProphecy); + $managerRegistryProphecy->getManagerForClass(RelatedDummy::class)->willReturn($entityManagerProphecy); + + $this->assertTrue(QueryChecker::hasOrderByOnToManyJoin($queryBuilder, $managerRegistryProphecy->reveal())); + } + + public function testHasJoinedToManyAssociationWithoutJoin() + { + $entityManagerProphecy = $this->prophesize(EntityManagerInterface::class); + + $queryBuilder = new QueryBuilder($entityManagerProphecy->reveal()); + $queryBuilder->select(['d']); + $queryBuilder->from(Dummy::class, 'd'); + + $managerRegistryProphecy = $this->prophesize(ManagerRegistry::class); + + $this->assertFalse(QueryChecker::hasJoinedToManyAssociation($queryBuilder, $managerRegistryProphecy->reveal())); + } + + public function testHasJoinedToManyAssociationWithJoinByAssociation() + { + $dummyMetadata = new ClassMetadata(Dummy::class); + $dummyMetadata->mapManyToMany([ + 'fieldName' => 'relatedDummies', + 'targetEntity' => RelatedDummy::class, + ]); + + $relatedDummyMetadata = new ClassMetadata(RelatedDummy::class); + + $entityManagerProphecy = $this->prophesize(EntityManagerInterface::class); + $entityManagerProphecy->getClassMetadata(Dummy::class)->willReturn($dummyMetadata); + $entityManagerProphecy->getClassMetadata(RelatedDummy::class)->willReturn($relatedDummyMetadata); + + $queryBuilder = new QueryBuilder($entityManagerProphecy->reveal()); + $queryBuilder->select(['d']); + $queryBuilder->from(Dummy::class, 'd'); + $queryBuilder->leftJoin('d.relatedDummies', 'a_1'); + + $managerRegistryProphecy = $this->prophesize(ManagerRegistry::class); + $managerRegistryProphecy->getManagerForClass(Dummy::class)->willReturn($entityManagerProphecy); + $managerRegistryProphecy->getManagerForClass(RelatedDummy::class)->willReturn($entityManagerProphecy); - $this->assertTrue(QueryChecker::hasOrderByOnToManyJoin($queryBuilder->reveal(), $managerRegistry->reveal())); + $this->assertTrue(QueryChecker::hasJoinedToManyAssociation($queryBuilder, $managerRegistryProphecy->reveal())); } } diff --git a/tests/Bridge/Doctrine/Orm/Util/QueryJoinParserTest.php b/tests/Bridge/Doctrine/Orm/Util/QueryJoinParserTest.php index 6e40f8eda98..fd43fc0bd8a 100644 --- a/tests/Bridge/Doctrine/Orm/Util/QueryJoinParserTest.php +++ b/tests/Bridge/Doctrine/Orm/Util/QueryJoinParserTest.php @@ -14,33 +14,73 @@ namespace ApiPlatform\Core\Tests\Bridge\Doctrine\Orm\Util; use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryJoinParser; +use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\Dummy; use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\RelatedDummy; use Doctrine\Common\Persistence\ManagerRegistry; -use Doctrine\Common\Persistence\ObjectManager; +use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\Mapping\ClassMetadata; use Doctrine\ORM\Query\Expr\Join; use Doctrine\ORM\Query\Expr\OrderBy; use Doctrine\ORM\QueryBuilder; -use phpmock\phpunit\PHPMock; use PHPUnit\Framework\TestCase; +/** + * @group legacy + */ class QueryJoinParserTest extends TestCase { - use PHPMock; + /** + * @group legacy + * @expectedDeprecation The use of "ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryJoinParser::getClassMetadataFromJoinAlias()" is deprecated since 2.4 and will be removed in 3.0. Use "ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryBuilderHelper::getEntityClassByAlias()" instead. + */ + public function testGetClassMetadataFromJoinAliasWithJoinByAssociation(): void + { + $dummyMetadata = new ClassMetadata(Dummy::class); + $dummyMetadata->mapManyToMany([ + 'fieldName' => 'relatedDummies', + 'targetEntity' => RelatedDummy::class, + ]); + + $relatedDummyMetadata = new ClassMetadata(RelatedDummy::class); + + $entityManagerProphecy = $this->prophesize(EntityManagerInterface::class); + $entityManagerProphecy->getClassMetadata(Dummy::class)->willReturn($dummyMetadata); + $entityManagerProphecy->getClassMetadata(RelatedDummy::class)->willReturn($relatedDummyMetadata); + + $queryBuilder = new QueryBuilder($entityManagerProphecy->reveal()); + $queryBuilder->from(Dummy::class, 'd'); + $queryBuilder->innerJoin('d.relatedDummies', 'a_1'); + + $managerRegistryProphecy = $this->prophesize(ManagerRegistry::class); + $managerRegistryProphecy->getManagerForClass(Dummy::class)->willReturn($entityManagerProphecy); + $managerRegistryProphecy->getManagerForClass(RelatedDummy::class)->willReturn($entityManagerProphecy); + + $actual = QueryJoinParser::getClassMetadataFromJoinAlias('a_1', $queryBuilder, $managerRegistryProphecy->reveal()); - public function testGetClassMetadataFromJoinAlias() + $this->assertEquals($relatedDummyMetadata, $actual); + } + + /** + * @group legacy + * @expectedDeprecation The use of "ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryJoinParser::getClassMetadataFromJoinAlias()" is deprecated since 2.4 and will be removed in 3.0. Use "ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryBuilderHelper::getEntityClassByAlias()" instead. + */ + public function testGetClassMetadataFromJoinAliasWithJoinByClass(): void { - $queryBuilder = $this->prophesize(QueryBuilder::class); - $queryBuilder->getRootEntities()->willReturn(['Dummy']); - $queryBuilder->getRootAliases()->willReturn(['d']); - $queryBuilder->getDQLPart('join')->willReturn(['a_1' => [new Join('INNER_JOIN', 'relatedDummy', 'a_1', null, 'a_1.name = r.name')]]); - $classMetadata = $this->prophesize(ClassMetadata::class); - $objectManager = $this->prophesize(ObjectManager::class); - $objectManager->getClassMetadata('Dummy')->willReturn($classMetadata->reveal()); - $managerRegistry = $this->prophesize(ManagerRegistry::class); - $managerRegistry->getManagerForClass('Dummy')->willReturn($objectManager->reveal()); - $metadata = QueryJoinParser::getClassMetadataFromJoinAlias('a_1', $queryBuilder->reveal(), $managerRegistry->reveal()); - $this->assertEquals($metadata, $classMetadata->reveal()); + $relatedDummyMetadata = new ClassMetadata(RelatedDummy::class); + + $entityManagerProphecy = $this->prophesize(EntityManagerInterface::class); + $entityManagerProphecy->getClassMetadata(RelatedDummy::class)->willReturn($relatedDummyMetadata); + + $queryBuilder = new QueryBuilder($entityManagerProphecy->reveal()); + $queryBuilder->from(Dummy::class, 'd'); + $queryBuilder->innerJoin(RelatedDummy::class, 'a_1', null, 'd.name = a_1.name'); + + $managerRegistryProphecy = $this->prophesize(ManagerRegistry::class); + $managerRegistryProphecy->getManagerForClass(RelatedDummy::class)->willReturn($entityManagerProphecy); + + $actual = QueryJoinParser::getClassMetadataFromJoinAlias('a_1', $queryBuilder, $managerRegistryProphecy->reveal()); + + $this->assertEquals($relatedDummyMetadata, $actual); } /**