From de87733de4142c905cec57737505ce76dc134ed4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Malte=20Hu=CC=88bner?= Date: Sat, 14 Mar 2026 09:40:19 +0100 Subject: [PATCH 1/4] Add PaginatedResult class for paginated query responses Introduces a result container that holds paginated data along with metadata: page, size, totalItems, and computed totalPages. Refs #25 Co-Authored-By: Claude Opus 4.6 (1M context) --- src/PaginatedResult/PaginatedResult.php | 44 +++++++++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 src/PaginatedResult/PaginatedResult.php diff --git a/src/PaginatedResult/PaginatedResult.php b/src/PaginatedResult/PaginatedResult.php new file mode 100644 index 0000000..59bfc52 --- /dev/null +++ b/src/PaginatedResult/PaginatedResult.php @@ -0,0 +1,44 @@ + */ + public function getData(): iterable + { + return $this->data; + } + + public function getPage(): int + { + return $this->page; + } + + public function getSize(): int + { + return $this->size; + } + + public function getTotalItems(): int + { + return $this->totalItems; + } + + public function getTotalPages(): int + { + if ($this->size === 0) { + return 0; + } + + return (int) ceil($this->totalItems / $this->size); + } +} From 21adae820106503f7b1e4e1f2f646f89903a506d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Malte=20Hu=CC=88bner?= Date: Sat, 14 Mar 2026 09:40:25 +0100 Subject: [PATCH 2/4] Add PageParameter and SizeParameter::getSize() for pagination support PageParameter provides a 0-based page parameter as a user-friendly alternative to the offset-based FromParameter. SizeParameter gains a getter so the DataQueryManager can extract the configured page size. Refs #25 Co-Authored-By: Claude Opus 4.6 (1M context) --- src/Parameter/PageParameter.php | 51 +++++++++++++++++++++++++++++++++ src/Parameter/SizeParameter.php | 5 ++++ 2 files changed, 56 insertions(+) create mode 100644 src/Parameter/PageParameter.php diff --git a/src/Parameter/PageParameter.php b/src/Parameter/PageParameter.php new file mode 100644 index 0000000..6675236 --- /dev/null +++ b/src/Parameter/PageParameter.php @@ -0,0 +1,51 @@ +page = $page; + return $this; + } + + public function getPage(): int + { + return $this->page; + } + + public function setPageSize(int $size): PageParameter + { + $this->size = $size; + return $this; + } + + #[\Override] + public function addToElasticQuery(Query $query): Query + { + return $query->setFrom($this->page * $this->size); + } + + #[\Override] + public function addToOrmQuery(QueryBuilder $queryBuilder): AbstractOrmQuery + { + $queryBuilder->setFirstResult($this->page * $this->size); + + return $queryBuilder->getQuery(); + } +} diff --git a/src/Parameter/SizeParameter.php b/src/Parameter/SizeParameter.php index 19b9fed..dddd07d 100644 --- a/src/Parameter/SizeParameter.php +++ b/src/Parameter/SizeParameter.php @@ -23,6 +23,11 @@ public function setSize(int $size): SizeParameter return $this; } + public function getSize(): int + { + return $this->size; + } + #[\Override] public function addToElasticQuery(Query $query): Query { From e90db03721f70ff46358e4e6aba30530ff117e4e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Malte=20Hu=CC=88bner?= Date: Sat, 14 Mar 2026 09:40:29 +0100 Subject: [PATCH 3/4] Implement paginated query execution for Doctrine ORM and Elasticsearch Doctrine ORM uses Doctrine\ORM\Tools\Pagination\Paginator for efficient total count queries. Elasticsearch uses the FOS Elastica paginator adapter to retrieve results and total count in a single operation. Refs #25 Co-Authored-By: Claude Opus 4.6 (1M context) --- src/Finder/Finder.php | 82 ++++++++++++++++++++++++++++++++++ src/Finder/FinderInterface.php | 3 ++ 2 files changed, 85 insertions(+) diff --git a/src/Finder/Finder.php b/src/Finder/Finder.php index 10c8f94..fa89099 100644 --- a/src/Finder/Finder.php +++ b/src/Finder/Finder.php @@ -3,7 +3,11 @@ namespace MalteHuebner\DataQueryBundle\Finder; use Doctrine\ORM\EntityManagerInterface; +use Doctrine\ORM\Tools\Pagination\Paginator; use FOS\ElasticaBundle\Repository; +use MalteHuebner\DataQueryBundle\PaginatedResult\PaginatedResult; +use MalteHuebner\DataQueryBundle\Parameter\FromParameter; +use MalteHuebner\DataQueryBundle\Parameter\PageParameter; use MalteHuebner\DataQueryBundle\Parameter\ParameterInterface; use MalteHuebner\DataQueryBundle\Parameter\SizeParameter; use MalteHuebner\DataQueryBundle\Query\ElasticQueryInterface; @@ -96,4 +100,82 @@ protected function executeOrmQuery(array $queryList, array $parameterList): arra return $qb->getQuery()->getResult(); } + + #[\Override] + public function executePaginatedQuery(array $queryList, array $parameterList, int $page, int $size): PaginatedResult + { + if ($this->entityManager) { + return $this->executePaginatedOrmQuery($queryList, $parameterList, $page, $size); + } + + if ($this->repository) { + return $this->executePaginatedElasticQuery($queryList, $parameterList, $page, $size); + } + + return new PaginatedResult([], $page, $size, 0); + } + + protected function executePaginatedElasticQuery(array $queryList, array $parameterList, int $page, int $size): PaginatedResult + { + $boolQuery = new \Elastica\Query\BoolQuery(); + + /** @var ElasticQueryInterface $query */ + foreach ($queryList as $query) { + if ($query instanceof QueryInterface) { + $boolQuery->addMust($query->createElasticQuery()); + } + } + + $query = new \Elastica\Query($boolQuery); + $query->setFrom($page * $size); + $query->setSize($size); + + /** @var ParameterInterface $parameter */ + foreach ($parameterList as $parameter) { + if ($parameter instanceof ParameterInterface) { + $query = $parameter->addToElasticQuery($query); + } + } + + $paginatorAdapter = $this->repository->createPaginatorAdapter($query); + $totalItems = $paginatorAdapter->getNbResults(); + $results = $paginatorAdapter->getSlice(0, $size)->toArray(); + + return new PaginatedResult($results, $page, $size, $totalItems); + } + + protected function executePaginatedOrmQuery(array $queryList, array $parameterList, int $page, int $size): PaginatedResult + { + $qb = $this->entityManager->createQueryBuilder() + ->select('e') + ->from($this->fqcn, 'e') + ; + + /** @var OrmQueryInterface $query */ + foreach ($queryList as $query) { + if ($query instanceof OrmQueryInterface) { + $qb = $query->createOrmQuery($qb); + } + } + + /** @var ParameterInterface $parameter */ + foreach ($parameterList as $parameter) { + if ($parameter instanceof SizeParameter || $parameter instanceof FromParameter || $parameter instanceof PageParameter) { + continue; + } + + if ($parameter instanceof ParameterInterface && method_exists($parameter, 'addToOrmQuery')) { + $parameter->addToOrmQuery($qb); + } + } + + $qb->setFirstResult($page * $size); + $qb->setMaxResults($size); + + $paginator = new Paginator($qb->getQuery()); + $totalItems = count($paginator); + $data = iterator_to_array($paginator); + + return new PaginatedResult($data, $page, $size, $totalItems); + } } diff --git a/src/Finder/FinderInterface.php b/src/Finder/FinderInterface.php index 6665b08..00bf72e 100644 --- a/src/Finder/FinderInterface.php +++ b/src/Finder/FinderInterface.php @@ -2,7 +2,10 @@ namespace MalteHuebner\DataQueryBundle\Finder; +use MalteHuebner\DataQueryBundle\PaginatedResult\PaginatedResult; + interface FinderInterface { public function executeQuery(array $queryList, array $parameterList): array; + public function executePaginatedQuery(array $queryList, array $parameterList, int $page, int $size): PaginatedResult; } From 7e9e293137afd3c28a1d63a0aac0a1f3b91fc5bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Malte=20Hu=CC=88bner?= Date: Sat, 14 Mar 2026 09:40:38 +0100 Subject: [PATCH 4/4] Add paginatedQuery() to DataQueryManager and update service config Exposes the new paginatedQuery() method on the DataQueryManagerInterface, wires PaginatedResult into service autodiscovery, and baselines the optional FOS Elastica createPaginatorAdapter() call in PHPStan. Closes #25 Co-Authored-By: Claude Opus 4.6 (1M context) --- config/services.yaml | 2 +- phpstan-baseline.neon | 5 ++++ src/DataQueryManager/DataQueryManager.php | 27 +++++++++++++++++++ .../DataQueryManagerInterface.php | 2 ++ 4 files changed, 35 insertions(+), 1 deletion(-) diff --git a/config/services.yaml b/config/services.yaml index cc9dd48..76970d4 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -5,7 +5,7 @@ services: public: true MalteHuebner\DataQueryBundle\: - resource: '../src/{DataQueryManager,Factory,FieldList,FinderFactory,Manager,Parameter,Query,RequestParameterList,Validator}' + resource: '../src/{DataQueryManager,Factory,FieldList,FinderFactory,Manager,PaginatedResult,Parameter,Query,RequestParameterList,Validator}' exclude: '../src/{DependencyInjection,Factory/ConflictResolver,Factory/ValueAssigner/ValueType.php,RequestParameterList/ArrayToListConverter.php,RequestParameterList/QueryStringToListConverter.php,RequestParameterList/RequestToListConverter.php,tests,MalteHuebnerDataQueryBundle}' Psr\Container\ContainerInterface: diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index a527b6a..5bcf714 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -55,6 +55,11 @@ parameters: count: 2 path: src/FieldList/QueryFieldList/QueryFieldListFactory.php + - + message: "#^Call to method createPaginatorAdapter\\(\\) on an unknown class FOS\\\\ElasticaBundle\\\\Repository\\.$#" + count: 1 + path: src/Finder/Finder.php + - message: "#^Call to method find\\(\\) on an unknown class FOS\\\\ElasticaBundle\\\\Repository\\.$#" count: 1 diff --git a/src/DataQueryManager/DataQueryManager.php b/src/DataQueryManager/DataQueryManager.php index 0666283..1389805 100644 --- a/src/DataQueryManager/DataQueryManager.php +++ b/src/DataQueryManager/DataQueryManager.php @@ -5,6 +5,9 @@ use MalteHuebner\DataQueryBundle\Factory\ParameterFactory\ParameterFactoryInterface; use MalteHuebner\DataQueryBundle\Factory\QueryFactory\QueryFactoryInterface; use MalteHuebner\DataQueryBundle\FinderFactory\FinderFactoryInterface; +use MalteHuebner\DataQueryBundle\PaginatedResult\PaginatedResult; +use MalteHuebner\DataQueryBundle\Parameter\PageParameter; +use MalteHuebner\DataQueryBundle\Parameter\SizeParameter; use MalteHuebner\DataQueryBundle\RequestParameterList\RequestParameterList; class DataQueryManager implements DataQueryManagerInterface @@ -27,4 +30,28 @@ public function query(RequestParameterList $requestParameterList, string $entity return $finder->executeQuery($queryList, $parameterList); } + + #[\Override] + public function paginatedQuery(RequestParameterList $requestParameterList, string $entityFqcn): PaginatedResult + { + $queryList = $this->queryFactory->setEntityFqcn($entityFqcn)->createFromList($requestParameterList); + $parameterList = $this->parameterFactory->setEntityFqcn($entityFqcn)->createFromList($requestParameterList); + + $page = 0; + $size = 10; + + foreach ($parameterList as $parameter) { + if ($parameter instanceof PageParameter) { + $page = $parameter->getPage(); + } + + if ($parameter instanceof SizeParameter) { + $size = $parameter->getSize(); + } + } + + $finder = $this->finderFactory->createFinderForFqcn($entityFqcn); + + return $finder->executePaginatedQuery($queryList, $parameterList, $page, $size); + } } diff --git a/src/DataQueryManager/DataQueryManagerInterface.php b/src/DataQueryManager/DataQueryManagerInterface.php index b41801e..35cd935 100644 --- a/src/DataQueryManager/DataQueryManagerInterface.php +++ b/src/DataQueryManager/DataQueryManagerInterface.php @@ -2,9 +2,11 @@ namespace MalteHuebner\DataQueryBundle\DataQueryManager; +use MalteHuebner\DataQueryBundle\PaginatedResult\PaginatedResult; use MalteHuebner\DataQueryBundle\RequestParameterList\RequestParameterList; interface DataQueryManagerInterface { public function query(RequestParameterList $requestParameterList, string $entityFqcn): array; + public function paginatedQuery(RequestParameterList $requestParameterList, string $entityFqcn): PaginatedResult; }