Skip to content

Commit f91d3d8

Browse files
committed
Allow limit 0 for MongoDB
1 parent 63716ce commit f91d3d8

File tree

5 files changed

+54
-12
lines changed

5 files changed

+54
-12
lines changed

features/hydra/collection.feature

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -396,7 +396,6 @@ Feature: Collections support
396396
}
397397
"""
398398

399-
@!mongodb
400399
@createSchema
401400
Scenario: Allow passing 0 to `itemsPerPage`
402401
When I send a "GET" request to "/dummies?itemsPerPage=0"

src/Bridge/Doctrine/MongoDbOdm/Extension/PaginationExtension.php

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -64,12 +64,18 @@ public function applyToCollection(Builder $aggregationBuilder, string $resourceC
6464
throw new RuntimeException(sprintf('The repository for "%s" must be an instance of "%s".', $resourceClass, DocumentRepository::class));
6565
}
6666

67+
$resultsAggregationBuilder = $repository->createAggregationBuilder()->skip($offset);
68+
if ($limit > 0) {
69+
$resultsAggregationBuilder->limit($limit);
70+
} else {
71+
// Results have to be 0 but MongoDB does not support a limit equal to 0.
72+
$resultsAggregationBuilder->match()->field(Paginator::LIMIT_ZERO_MARKER_FIELD)->equals(Paginator::LIMIT_ZERO_MARKER);
73+
}
74+
6775
$aggregationBuilder
6876
->facet()
6977
->field('results')->pipeline(
70-
$repository->createAggregationBuilder()
71-
->skip($offset)
72-
->limit($limit)
78+
$resultsAggregationBuilder
7379
)
7480
->field('count')->pipeline(
7581
$repository->createAggregationBuilder()

src/Bridge/Doctrine/MongoDbOdm/Paginator.php

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@
2828
*/
2929
final class Paginator implements \IteratorAggregate, PaginatorInterface
3030
{
31+
public const LIMIT_ZERO_MARKER_FIELD = '___';
32+
public const LIMIT_ZERO_MARKER = 'limit0';
33+
3134
/**
3235
* @var Iterator
3336
*/
@@ -76,7 +79,7 @@ public function __construct(Iterator $mongoDbOdmIterator, UnitOfWork $unitOfWork
7679
* skip/limit parameters of the query, the values set in the facet stage are used instead.
7780
*/
7881
$this->firstResult = $this->getStageInfo($resultsFacetInfo, '$skip');
79-
$this->maxResults = $this->getStageInfo($resultsFacetInfo, '$limit');
82+
$this->maxResults = $this->hasLimitZeroStage($resultsFacetInfo) ? 0 : $this->getStageInfo($resultsFacetInfo, '$limit');
8083
$this->totalItems = $mongoDbOdmIterator->toArray()[0]['count'][0]['count'] ?? 0;
8184
}
8285

@@ -85,6 +88,10 @@ public function __construct(Iterator $mongoDbOdmIterator, UnitOfWork $unitOfWork
8588
*/
8689
public function getCurrentPage(): float
8790
{
91+
if (0 >= $this->maxResults) {
92+
return 1.;
93+
}
94+
8895
return floor($this->firstResult / $this->maxResults) + 1.;
8996
}
9097

@@ -93,6 +100,10 @@ public function getCurrentPage(): float
93100
*/
94101
public function getLastPage(): float
95102
{
103+
if (0 >= $this->maxResults) {
104+
return 1.;
105+
}
106+
96107
return ceil($this->totalItems / $this->maxResults) ?: 1.;
97108
}
98109

@@ -161,4 +172,15 @@ private function getStageInfo(array $resultsFacetInfo, string $stage): int
161172

162173
throw new InvalidArgumentException("$stage stage was not applied to the facet stage of the aggregation pipeline.");
163174
}
175+
176+
private function hasLimitZeroStage(array $resultsFacetInfo): bool
177+
{
178+
foreach ($resultsFacetInfo as $resultFacetInfo) {
179+
if (self::LIMIT_ZERO_MARKER === ($resultFacetInfo['$match'][self::LIMIT_ZERO_MARKER_FIELD] ?? null)) {
180+
return true;
181+
}
182+
}
183+
184+
return false;
185+
}
164186
}

tests/Bridge/Doctrine/MongoDbOdm/Extension/PaginationExtensionTest.php

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
namespace ApiPlatform\Core\Tests\Bridge\Doctrine\MongoDbOdm\Extension;
1515

1616
use ApiPlatform\Core\Bridge\Doctrine\MongoDbOdm\Extension\PaginationExtension;
17+
use ApiPlatform\Core\Bridge\Doctrine\MongoDbOdm\Paginator;
1718
use ApiPlatform\Core\DataProvider\Pagination;
1819
use ApiPlatform\Core\DataProvider\PaginatorInterface;
1920
use ApiPlatform\Core\DataProvider\PartialPaginatorInterface;
@@ -24,7 +25,7 @@
2425
use Doctrine\ODM\MongoDB\Aggregation\Builder;
2526
use Doctrine\ODM\MongoDB\Aggregation\Stage\Count;
2627
use Doctrine\ODM\MongoDB\Aggregation\Stage\Facet;
27-
use Doctrine\ODM\MongoDB\Aggregation\Stage\Limit;
28+
use Doctrine\ODM\MongoDB\Aggregation\Stage\Match;
2829
use Doctrine\ODM\MongoDB\Aggregation\Stage\Skip;
2930
use Doctrine\ODM\MongoDB\DocumentManager;
3031
use Doctrine\ODM\MongoDB\Iterator\Iterator;
@@ -391,10 +392,15 @@ private function getPaginationExtensionResult()
391392

392393
private function mockAggregationBuilder($expectedOffset, $expectedLimit)
393394
{
394-
$limitProphecy = $this->prophesize(Limit::class);
395-
396395
$skipProphecy = $this->prophesize(Skip::class);
397-
$skipProphecy->limit($expectedLimit)->shouldBeCalled()->willReturn($limitProphecy->reveal());
396+
if ($expectedLimit > 0) {
397+
$skipProphecy->limit($expectedLimit)->shouldBeCalled();
398+
} else {
399+
$matchProphecy = $this->prophesize(Match::class);
400+
$matchProphecy->field(Paginator::LIMIT_ZERO_MARKER_FIELD)->shouldBeCalled()->willReturn($matchProphecy);
401+
$matchProphecy->equals(Paginator::LIMIT_ZERO_MARKER)->shouldBeCalled();
402+
$skipProphecy->match()->shouldBeCalled()->willReturn($matchProphecy->reveal());
403+
}
398404

399405
$resultsAggregationBuilderProphecy = $this->prophesize(Builder::class);
400406
$resultsAggregationBuilderProphecy->skip($expectedOffset)->shouldBeCalled()->willReturn($skipProphecy->reveal());
@@ -416,7 +422,7 @@ private function mockAggregationBuilder($expectedOffset, $expectedLimit)
416422
$this->managerRegistryProphecy->getManagerForClass('Foo')->shouldBeCalled()->willReturn($objectManagerProphecy->reveal());
417423

418424
$facetProphecy = $this->prophesize(Facet::class);
419-
$facetProphecy->pipeline($limitProphecy)->shouldBeCalled()->willReturn($facetProphecy);
425+
$facetProphecy->pipeline($skipProphecy)->shouldBeCalled()->willReturn($facetProphecy);
420426
$facetProphecy->pipeline($countProphecy)->shouldBeCalled()->willReturn($facetProphecy);
421427
$facetProphecy->field('count')->shouldBeCalled()->willReturn($facetProphecy);
422428
$facetProphecy->field('results')->shouldBeCalled()->willReturn($facetProphecy);

tests/Bridge/Doctrine/MongoDbOdm/PaginatorTest.php

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,15 @@ public function testInitializeWithLimitStageNotApplied()
7474
$this->getPaginatorWithMissingStage(true, true, true, true);
7575
}
7676

77+
public function testInitializeWithLimitZeroStageApplied()
78+
{
79+
$paginator = $this->getPaginator(0, 5, 0, true);
80+
81+
$this->assertEquals(1, $paginator->getCurrentPage());
82+
$this->assertEquals(1, $paginator->getLastPage());
83+
$this->assertEquals(0, $paginator->getItemsPerPage());
84+
}
85+
7786
public function testInitializeWithNoCount()
7887
{
7988
$paginator = $this->getPaginatorWithNoCount();
@@ -90,15 +99,15 @@ public function testGetIterator()
9099
$this->assertSame($paginator->getIterator(), $paginator->getIterator(), 'Iterator should be cached');
91100
}
92101

93-
private function getPaginator($firstResult = 1, $maxResults = 15, $totalItems = 42)
102+
private function getPaginator($firstResult = 1, $maxResults = 15, $totalItems = 42, $limitZero = false)
94103
{
95104
$iterator = $this->prophesize(Iterator::class);
96105
$pipeline = [
97106
[
98107
'$facet' => [
99108
'results' => [
100109
['$skip' => $firstResult],
101-
['$limit' => $maxResults],
110+
$limitZero ? ['$match' => [Paginator::LIMIT_ZERO_MARKER_FIELD => Paginator::LIMIT_ZERO_MARKER]] : ['$limit' => $maxResults],
102111
],
103112
'count' => [
104113
['$count' => 'count'],

0 commit comments

Comments
 (0)