diff --git a/.php_cs b/.php_cs index b3d5d8e..5c5ac95 100644 --- a/.php_cs +++ b/.php_cs @@ -6,7 +6,7 @@ include __DIR__ . '/vendor/autoload.php'; return Paysera\PhpCsFixerConfig\Config\PayseraConventionsConfig::create() ->setDefaultFinder(['src', 'tests'], []) ->setRiskyRules([ - 'Paysera/php_basic_comment_php_doc_necessity' => false, + 'Paysera/php_basic_comment_php_doc_contents' => false, 'Paysera/php_basic_code_style_splitting_in_several_lines' => false, 'Paysera/php_basic_code_style_chained_method_calls' => false, ]) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9ae9537..31c0777 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,3 +4,6 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## 1.0.0 +### Fixed +- support single `group-by` statement in `ResultProvider:findCount` - this enables to properly get count of grouped statements. diff --git a/src/Exception/InvalidGroupByException.php b/src/Exception/InvalidGroupByException.php new file mode 100644 index 0000000..c8b4c24 --- /dev/null +++ b/src/Exception/InvalidGroupByException.php @@ -0,0 +1,29 @@ +groupBy = $groupBy; + } + + /** + * @return string + */ + public function getGroupBy(): string + { + return $this->groupBy; + } +} diff --git a/src/Service/Doctrine/ResultProvider.php b/src/Service/Doctrine/ResultProvider.php index 644e87d..024e460 100644 --- a/src/Service/Doctrine/ResultProvider.php +++ b/src/Service/Doctrine/ResultProvider.php @@ -6,6 +6,7 @@ use Doctrine\ORM\Query\Expr; use Doctrine\ORM\Query\Expr\Andx; use Doctrine\ORM\Query\Expr\Orx; +use Doctrine\ORM\Query\Expr\GroupBy; use Doctrine\ORM\Query\Expr\Comparison; use Doctrine\ORM\QueryBuilder; use Paysera\Pagination\Entity\Doctrine\AnalysedQuery; @@ -13,6 +14,7 @@ use Paysera\Pagination\Entity\Doctrine\ConfiguredQuery; use Paysera\Pagination\Entity\Pager; use Paysera\Pagination\Entity\Result; +use Paysera\Pagination\Exception\InvalidGroupByException; use Paysera\Pagination\Service\CursorBuilderInterface; class ResultProvider @@ -317,7 +319,62 @@ private function calculateTotalCount(Pager $filter, int $resultCount) private function findCount(AnalysedQuery $analysedQuery): int { $countQueryBuilder = $analysedQuery->cloneQueryBuilder(); + $groupByColumn = $this->getSingleValidGroupByColumn($countQueryBuilder); + + if ($groupByColumn !== null) { + return $this->findCountWithGroupBy($groupByColumn, $analysedQuery); + } + $countQueryBuilder->select(sprintf('count(%s)', $analysedQuery->getRootAlias())); return (int)$countQueryBuilder->getQuery()->getSingleScalarResult(); } + + private function findCountWithGroupBy(string $groupByColumn, AnalysedQuery $analysedQuery): int + { + $countQueryBuilder = $analysedQuery->cloneQueryBuilder(); + $countQueryBuilder + ->resetDQLPart('groupBy') + ->select(sprintf('count(distinct %s)', $groupByColumn)) + ; + + $nullQueryBuilder = $analysedQuery->cloneQueryBuilder() + ->resetDQLPart('groupBy') + ->select($analysedQuery->getRootAlias()) + ->setMaxResults(1) + ->andWhere($groupByColumn . ' is null') + ; + + $nonNullCount = (int)$countQueryBuilder->getQuery()->getSingleScalarResult(); + $nullExists = count($nullQueryBuilder->getQuery()->getScalarResult()); + + return $nonNullCount + $nullExists; + } + + /** + * @param QueryBuilder $queryBuilder + * @return string|null + */ + private function getSingleValidGroupByColumn(QueryBuilder $queryBuilder) + { + /** @var GroupBy[] $groupByParts */ + $groupByParts = $queryBuilder->getDQLPart('groupBy'); + + if (count($groupByParts) === 0) { + return null; + } + + if (count($groupByParts) > 1) { + $groupNames = array_map( + function (GroupBy $groupBy) { return $groupBy->getParts()[0]; }, + $groupByParts + ); + throw new InvalidGroupByException(implode(', ', $groupNames)); + } + + if (count($groupByParts) === 1 && count($groupByParts[0]->getParts()) > 1) { + throw new InvalidGroupByException(implode(', ', $groupByParts[0]->getParts())); + } + + return $groupByParts[0]->getParts()[0]; + } } diff --git a/tests/Functional/Fixtures/ParentTestEntity.php b/tests/Functional/Fixtures/ParentTestEntity.php index 21e6253..57dab31 100644 --- a/tests/Functional/Fixtures/ParentTestEntity.php +++ b/tests/Functional/Fixtures/ParentTestEntity.php @@ -32,6 +32,13 @@ class ParentTestEntity */ private $name; + /** + * @var string|null + * + * @Column(type="string", nullable=true) + */ + private $groupKey; + /** * @var ChildTestEntity[]|Collection * @@ -98,4 +105,23 @@ public function setChildren($children) return $this; } + + /** + * @return string|null + */ + public function getGroupKey() + { + return $this->groupKey; + } + + /** + * @param string|null $groupKey + * + * @return $this + */ + public function setGroupKey(string $groupKey): self + { + $this->groupKey = $groupKey; + return $this; + } } diff --git a/tests/Functional/Service/Doctrine/ResultProviderTest.php b/tests/Functional/Service/Doctrine/ResultProviderTest.php index 977317c..6058469 100644 --- a/tests/Functional/Service/Doctrine/ResultProviderTest.php +++ b/tests/Functional/Service/Doctrine/ResultProviderTest.php @@ -6,6 +6,7 @@ use Doctrine\ORM\QueryBuilder; use Paysera\Pagination\Entity\OrderingConfiguration; use Doctrine\ORM\EntityManager; +use Paysera\Pagination\Exception\InvalidGroupByException; use Paysera\Pagination\Service\Doctrine\QueryAnalyser; use Paysera\Pagination\Entity\Doctrine\ConfiguredQuery; use Paysera\Pagination\Entity\OrderingPair; @@ -36,10 +37,13 @@ protected function setUp() ); } - private function createTestData(EntityManager $entityManager) + private function createTestData(EntityManager $entityManager, int $groupEvery = 10) { for ($parentIndex = 0; $parentIndex < 30; $parentIndex++) { $parent = (new ParentTestEntity())->setName(sprintf('P%s', $parentIndex)); + if ($parentIndex % $groupEvery === 0) { + $parent->setGroupKey(sprintf('group_%s', $parentIndex)); + } $entityManager->persist($parent); } @@ -603,23 +607,79 @@ public function testGetTotalCountForQuery($expectedResult, QueryBuilder $queryBu $this->assertSame($expectedResult, $result); } - public function getResultProviderForGetTotalCountForQuery() + public function testGetTotalCountForQueryThrowsExceptionWithMoreThanOneGroupByArgument() { $entityManager = $this->createTestEntityManager(); $this->createTestData($entityManager); $queryBuilder = $entityManager->createQueryBuilder() ->select('p') ->from('PaginationTest:ParentTestEntity', 'p') + ->groupBy('p.groupKey', 'p.name') + ; + + $configuredQuery = (new ConfiguredQuery($queryBuilder))->setTotalCountNeeded(false); + + $this->expectException(InvalidGroupByException::class); + $this->resultProvider->getTotalCountForQuery($configuredQuery); + } + + public function testGetTotalCountForQueryThrowsExceptionWithMoreThanOneGroupByExpression() + { + $entityManager = $this->createTestEntityManager(); + $this->createTestData($entityManager); + $queryBuilder = $entityManager->createQueryBuilder() + ->select('p') + ->from('PaginationTest:ParentTestEntity', 'p') + ->groupBy('p.groupKey') + ->addGroupBy('p.name') + ; + + $configuredQuery = (new ConfiguredQuery($queryBuilder))->setTotalCountNeeded(false); + + $this->expectException(InvalidGroupByException::class); + $this->resultProvider->getTotalCountForQuery($configuredQuery); + } + + public function testGetTotalCountForQueryGetsCorrectCountWhenNoNullsAreInResult() + { + $entityManager = $this->createTestEntityManager(); + $this->createTestData($entityManager, 1); + $queryBuilder = $entityManager->createQueryBuilder() + ->select('p') + ->from('PaginationTest:ParentTestEntity', 'p') + ->groupBy('p.groupKey') + ; + + $configuredQuery = (new ConfiguredQuery($queryBuilder))->setTotalCountNeeded(false); + $this->assertSame(30, $this->resultProvider->getTotalCountForQuery($configuredQuery)); + } + + public function getResultProviderForGetTotalCountForQuery() + { + $entityManager = $this->createTestEntityManager(); + $this->createTestData($entityManager); + $queryBuilder1 = $entityManager->createQueryBuilder() + ->select('p') + ->from('PaginationTest:ParentTestEntity', 'p') + ; + $queryBuilder2 = $entityManager->createQueryBuilder() + ->select('p') + ->from('PaginationTest:ParentTestEntity', 'p') + ->groupBy('p.groupKey') ; return [ [ 30, - $queryBuilder, + $queryBuilder1, ], [ 11, - (clone $queryBuilder)->andWhere('p.name LIKE :name')->setParameter('name', 'P2%'), + (clone $queryBuilder1)->andWhere('p.name LIKE :name')->setParameter('name', 'P2%'), + ], + [ + 4, + $queryBuilder2, ], ]; }