-
-
Notifications
You must be signed in to change notification settings - Fork 514
Add Pagination classes to ODM #2992
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: 2.17.x
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,40 @@ | ||
| <?php | ||
|
|
||
| declare(strict_types=1); | ||
|
|
||
| namespace Doctrine\ODM\MongoDB\Pagination; | ||
|
|
||
| use Doctrine\ODM\MongoDB\Aggregation\Builder; | ||
| use Doctrine\ODM\MongoDB\Iterator\Iterator; | ||
|
|
||
| /** | ||
| * @template-covariant TValue | ||
| * @template-extends Paginator<TValue> | ||
| */ | ||
| final class AggregationCursorPaginator extends Paginator | ||
| { | ||
| public function __construct( | ||
| private readonly Builder $aggregation, | ||
| public readonly mixed $after = null, | ||
| public readonly int $perPage = 24, | ||
| public readonly string $field = 'id', | ||
| ) { | ||
| } | ||
|
|
||
| /** @return Iterator<TValue> */ | ||
| protected function getResultsForCurrentPage(): Iterator | ||
| { | ||
| $builder = clone $this->aggregation; | ||
|
|
||
| if ($this->after) { | ||
| $builder | ||
| ->match() | ||
| ->field($this->field)->gt($this->after); | ||
| } | ||
|
|
||
| return $builder | ||
| ->limit($this->perPage) | ||
| ->getAggregation() | ||
| ->getIterator(); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,58 @@ | ||
| <?php | ||
|
|
||
| declare(strict_types=1); | ||
|
|
||
| namespace Doctrine\ODM\MongoDB\Pagination; | ||
|
|
||
| use Countable; | ||
| use Doctrine\ODM\MongoDB\Aggregation\Builder; | ||
| use Doctrine\ODM\MongoDB\Iterator\Iterator; | ||
|
|
||
| use function ceil; | ||
| use function iterator_to_array; | ||
|
|
||
| /** | ||
| * @template-covariant TValue | ||
| * @template-extends Paginator<TValue> | ||
| */ | ||
| final class AggregationOffsetPaginator extends Paginator implements Countable | ||
| { | ||
| private int $pageCount; | ||
|
|
||
| /** @psalm-param positive-int $page */ | ||
| public function __construct( | ||
| private readonly Builder $aggregation, | ||
| public readonly int $page, | ||
| public readonly int $perPage = 24, | ||
| ) { | ||
| } | ||
|
|
||
| public function count(): int | ||
| { | ||
| return $this->pageCount ??= $this->getNumberOfPages(); | ||
| } | ||
|
|
||
| private function getNumberOfPages(): int | ||
| { | ||
| $builder = clone $this->aggregation; | ||
| $results = $builder | ||
| ->hydrate(null) | ||
| ->count('numDocuments') | ||
| ->getAggregation() | ||
| ->getIterator(); | ||
| $numResults = iterator_to_array($results)[0]['numDocuments'] ?? 0; | ||
|
|
||
| return (int) ceil($numResults / $this->perPage); | ||
| } | ||
|
|
||
| /** @return Iterator<TValue> */ | ||
| protected function getResultsForCurrentPage(): Iterator | ||
| { | ||
| $builder = clone $this->aggregation; | ||
| $builder | ||
| ->skip(($this->page - 1) * $this->perPage) | ||
| ->limit($this->perPage); | ||
|
|
||
| return $builder->getAggregation()->getIterator(); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| <?php | ||
|
|
||
| declare(strict_types=1); | ||
|
|
||
| namespace Doctrine\ODM\MongoDB\Pagination; | ||
|
|
||
| use Doctrine\ODM\MongoDB\Iterator\Iterator; | ||
| use IteratorAggregate; | ||
|
|
||
| /** | ||
| * @template-covariant TValue | ||
| * @template-implements IteratorAggregate<mixed, TValue> | ||
| */ | ||
| abstract class Paginator implements IteratorAggregate | ||
| { | ||
| /** @return Iterator<TValue> */ | ||
| abstract protected function getResultsForCurrentPage(): Iterator; | ||
|
|
||
| /** @return Iterator<TValue> */ | ||
| final public function getIterator(): Iterator | ||
| { | ||
| return $this->getResultsForCurrentPage(); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,39 @@ | ||
| <?php | ||
|
|
||
| declare(strict_types=1); | ||
|
|
||
| namespace Doctrine\ODM\MongoDB\Pagination; | ||
|
|
||
| use Doctrine\ODM\MongoDB\Iterator\Iterator; | ||
| use Doctrine\ODM\MongoDB\Query\Builder; | ||
|
|
||
| /** | ||
| * @template-covariant TValue | ||
| * @template-extends Paginator<TValue> | ||
| */ | ||
| final class QueryCursorPaginator extends Paginator | ||
| { | ||
| public function __construct( | ||
| private readonly Builder $query, | ||
| public readonly mixed $after = null, | ||
| public readonly int $perPage = 24, | ||
| public readonly string $field = 'id', | ||
| ) { | ||
| } | ||
|
|
||
| /** @return Iterator<TValue> */ | ||
| protected function getResultsForCurrentPage(): Iterator | ||
| { | ||
| $builder = clone $this->query; | ||
|
|
||
| if ($this->after) { | ||
| $builder | ||
| ->field($this->field)->gt($this->after); | ||
| } | ||
|
|
||
| return $builder | ||
| ->limit($this->perPage) | ||
| ->getQuery() | ||
| ->getIterator(); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,55 @@ | ||
| <?php | ||
|
|
||
| declare(strict_types=1); | ||
|
|
||
| namespace Doctrine\ODM\MongoDB\Pagination; | ||
|
|
||
| use Countable; | ||
| use Doctrine\ODM\MongoDB\Iterator\Iterator; | ||
| use Doctrine\ODM\MongoDB\Query\Builder; | ||
|
|
||
| use function ceil; | ||
|
|
||
| /** | ||
| * @template-covariant TValue | ||
| * @template-extends Paginator<TValue> | ||
| */ | ||
| final class QueryOffsetPaginator extends Paginator implements Countable | ||
| { | ||
| private int $pageCount; | ||
|
|
||
| /** @psalm-param positive-int $page */ | ||
| public function __construct( | ||
| private readonly Builder $query, | ||
| public readonly int $page, | ||
| public readonly int $perPage = 24, | ||
| ) { | ||
| } | ||
|
|
||
| public function count(): int | ||
| { | ||
| return $this->pageCount ??= $this->getNumberOfPages(); | ||
| } | ||
|
|
||
| private function getNumberOfPages(): int | ||
| { | ||
| $builder = clone $this->query; | ||
|
|
||
| return (int) ceil($builder | ||
| ->hydrate(false) | ||
| ->count() | ||
| ->getQuery() | ||
| ->execute() / $this->perPage); | ||
|
Comment on lines
+38
to
+42
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is immediately forgetting the exact number of pages. I think it would be better to make this value accessible with a method such as
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We can add something like this. Since we're discussing this, how do you feel about the paginator count being the number of pages? I felt that made more sense than returning the number of results (or worse, the number of items on the current page). If we don't want to change the |
||
| } | ||
|
|
||
| /** @return Iterator<TValue> */ | ||
| protected function getResultsForCurrentPage(): Iterator | ||
| { | ||
| $builder = clone $this->query; | ||
| $builder | ||
| ->skip(($this->page - 1) * $this->perPage) | ||
| ->limit($this->perPage); | ||
|
|
||
| return $builder->getQuery()->getIterator(); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,102 @@ | ||
| <?php | ||
|
|
||
| declare(strict_types=1); | ||
|
|
||
| namespace Doctrine\Tests\ODM\MongoDB\Pagination; | ||
|
|
||
| use Doctrine\ODM\MongoDB\Aggregation\Builder; | ||
| use Doctrine\ODM\MongoDB\Pagination\AggregationCursorPaginator; | ||
| use Doctrine\ODM\MongoDB\Tests\BaseTestCase; | ||
| use Documents\Tag; | ||
|
|
||
| use function array_fill; | ||
| use function iterator_to_array; | ||
|
|
||
| class AggregationCursorPaginatorTest extends BaseTestCase | ||
| { | ||
| protected function setUp(): void | ||
| { | ||
| parent::setUp(); | ||
|
|
||
| $this->prepareData(); | ||
| } | ||
|
|
||
| protected function tearDown(): void | ||
| { | ||
| $this->dm->getDocumentCollection(Tag::class)->drop(); | ||
|
|
||
| parent::tearDown(); | ||
| } | ||
|
|
||
| public function testFirstPage(): void | ||
| { | ||
| $paginator = new AggregationCursorPaginator($this->createBuilder()); | ||
|
|
||
| $results = iterator_to_array($paginator); | ||
| $this->assertCount(24, $results); | ||
|
|
||
| $this->assertSame('0', $results[0]->name); | ||
| $this->assertSame('3', $results[2]->name); | ||
| } | ||
|
|
||
| public function testThirdPage(): void | ||
| { | ||
| $last = $this->createBuilder()->skip(47)->limit(24)->getAggregation()->getSingleResult(); | ||
|
|
||
| $results = iterator_to_array(new AggregationCursorPaginator($this->createBuilder(), $last->id)); | ||
| $this->assertCount(24, $results); | ||
|
|
||
| $this->assertSame('49', $results[0]->name); | ||
| } | ||
|
|
||
| public function testPartialPage(): void | ||
| { | ||
| $last = $this->createBuilder()->skip(95)->limit(24)->getAggregation()->getSingleResult(); | ||
|
|
||
| $paginator = new AggregationCursorPaginator($this->createBuilder(), $last->id); | ||
|
|
||
| $results = iterator_to_array($paginator); | ||
| $this->assertCount(3, $results); | ||
|
|
||
| $this->assertSame('97', $results[0]->name); | ||
| } | ||
|
|
||
| public function testDifferentPerPage(): void | ||
| { | ||
| $paginator = new AggregationCursorPaginator($this->createBuilder(), perPage: 12); | ||
|
|
||
| $results = iterator_to_array($paginator); | ||
| $this->assertCount(12, $results); | ||
| } | ||
|
|
||
| public function testDifferentField(): void | ||
| { | ||
| $paginator = new AggregationCursorPaginator($this->createBuilder(), after: '5', field: 'name'); | ||
|
|
||
| $results = iterator_to_array($paginator); | ||
| $this->assertCount(24, $results); | ||
|
|
||
| $this->assertSame('6', $results[0]->name); | ||
| } | ||
|
|
||
| private function prepareData(): void | ||
| { | ||
| foreach (array_fill(0, 100, true) as $key => $t) { | ||
| $this->dm->persist(new Tag((string) $key)); | ||
| } | ||
|
|
||
| $this->dm->flush(); | ||
| } | ||
|
|
||
| private function createBuilder(): Builder | ||
| { | ||
| $builder = $this->dm->createAggregationBuilder(Tag::class); | ||
| $builder | ||
| ->hydrate(Tag::class) | ||
| ->match() | ||
| ->field('name')->notEqual('2') | ||
| ->sort('id', 1); | ||
|
|
||
| return $builder; | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What's the difference between
hydrate(null)andhydrate(false)used in the query paginators?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The type, probably nothing else. We should eventually have a
string|falsetype to control this.