From 6da691b3b95cfc5c8175835df9e9e11b06f723e8 Mon Sep 17 00:00:00 2001 From: Priyadi Iman Nurcahyo <1102197+priyadi@users.noreply.github.com> Date: Sun, 11 Feb 2024 18:14:58 +0700 Subject: [PATCH] feat: paginators for Doctrine Collection & Selectable --- src/Doctrine/Common/CollectionPaginator.php | 98 ++++++++++++++++ src/Doctrine/Common/SelectablePaginator.php | 105 ++++++++++++++++++ .../Common/SelectablePartialPaginator.php | 82 ++++++++++++++ .../Common/Tests/CollectionPaginatorTest.php | 47 ++++++++ .../Common/Tests/SelectablePaginatorTest.php | 47 ++++++++ .../Tests/SelectablePartialPaginatorTest.php | 45 ++++++++ 6 files changed, 424 insertions(+) create mode 100644 src/Doctrine/Common/CollectionPaginator.php create mode 100644 src/Doctrine/Common/SelectablePaginator.php create mode 100644 src/Doctrine/Common/SelectablePartialPaginator.php create mode 100644 src/Doctrine/Common/Tests/CollectionPaginatorTest.php create mode 100644 src/Doctrine/Common/Tests/SelectablePaginatorTest.php create mode 100644 src/Doctrine/Common/Tests/SelectablePartialPaginatorTest.php diff --git a/src/Doctrine/Common/CollectionPaginator.php b/src/Doctrine/Common/CollectionPaginator.php new file mode 100644 index 00000000000..8b6fabdd5e4 --- /dev/null +++ b/src/Doctrine/Common/CollectionPaginator.php @@ -0,0 +1,98 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Doctrine\Common; + +use ApiPlatform\State\Pagination\PaginatorInterface; +use Doctrine\Common\Collections\ReadableCollection; + +/** + * @template T of object + * + * @implements PaginatorInterface + * @implements \IteratorAggregate + */ +final class CollectionPaginator implements \IteratorAggregate, PaginatorInterface +{ + /** + * @var array + */ + private readonly array $items; + private readonly float $totalItems; + + /** + * @param ReadableCollection $collection + */ + public function __construct( + readonly ReadableCollection $collection, + private readonly float $currentPage, + private readonly float $itemsPerPage + ) { + $this->items = $collection->slice((int) (($currentPage - 1) * $itemsPerPage), $itemsPerPage > 0 ? (int) $itemsPerPage : null); + $this->totalItems = $collection->count(); + } + + /** + * {@inheritdoc} + */ + public function getCurrentPage(): float + { + return $this->currentPage; + } + + /** + * {@inheritdoc} + */ + public function getLastPage(): float + { + if (0. >= $this->itemsPerPage) { + return 1.; + } + + return max(ceil($this->totalItems / $this->itemsPerPage) ?: 1., 1.); + } + + /** + * {@inheritdoc} + */ + public function getItemsPerPage(): float + { + return $this->itemsPerPage; + } + + /** + * {@inheritdoc} + */ + public function getTotalItems(): float + { + return $this->totalItems; + } + + /** + * {@inheritdoc} + */ + public function count(): int + { + return \count($this->items); + } + + /** + * {@inheritdoc} + * + * @return \Traversable + */ + public function getIterator(): \Traversable + { + yield from $this->items; + } +} diff --git a/src/Doctrine/Common/SelectablePaginator.php b/src/Doctrine/Common/SelectablePaginator.php new file mode 100644 index 00000000000..bd37ce554a0 --- /dev/null +++ b/src/Doctrine/Common/SelectablePaginator.php @@ -0,0 +1,105 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Doctrine\Common; + +use ApiPlatform\State\Pagination\PaginatorInterface; +use Doctrine\Common\Collections\Criteria; +use Doctrine\Common\Collections\ReadableCollection; +use Doctrine\Common\Collections\Selectable; + +/** + * @template T of object + * + * @implements PaginatorInterface + * @implements \IteratorAggregate + */ +final class SelectablePaginator implements \IteratorAggregate, PaginatorInterface +{ + /** + * @var ReadableCollection + */ + private readonly ReadableCollection $slicedCollection; + private readonly float $totalItems; + + /** + * @param Selectable $selectable + */ + public function __construct( + readonly Selectable $selectable, + private readonly float $currentPage, + private readonly float $itemsPerPage + ) { + $this->totalItems = $this->selectable instanceof \Countable ? $this->selectable->count() : $this->selectable->matching(Criteria::create())->count(); + + $criteria = Criteria::create() + ->setFirstResult((int) (($currentPage - 1) * $itemsPerPage)) + ->setMaxResults($itemsPerPage > 0 ? (int) $itemsPerPage : null); + + $this->slicedCollection = $selectable->matching($criteria); + } + + /** + * {@inheritdoc} + */ + public function getCurrentPage(): float + { + return $this->currentPage; + } + + /** + * {@inheritdoc} + */ + public function getLastPage(): float + { + if (0. >= $this->itemsPerPage) { + return 1.; + } + + return max(ceil($this->totalItems / $this->itemsPerPage) ?: 1., 1.); + } + + /** + * {@inheritdoc} + */ + public function getItemsPerPage(): float + { + return $this->itemsPerPage; + } + + /** + * {@inheritdoc} + */ + public function getTotalItems(): float + { + return $this->totalItems; + } + + /** + * {@inheritdoc} + */ + public function count(): int + { + return $this->slicedCollection->count(); + } + + /** + * {@inheritdoc} + * + * @return \Traversable + */ + public function getIterator(): \Traversable + { + return $this->slicedCollection->getIterator(); + } +} diff --git a/src/Doctrine/Common/SelectablePartialPaginator.php b/src/Doctrine/Common/SelectablePartialPaginator.php new file mode 100644 index 00000000000..87b9c1bad7c --- /dev/null +++ b/src/Doctrine/Common/SelectablePartialPaginator.php @@ -0,0 +1,82 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Doctrine\Common; + +use ApiPlatform\State\Pagination\PartialPaginatorInterface; +use Doctrine\Common\Collections\Criteria; +use Doctrine\Common\Collections\ReadableCollection; +use Doctrine\Common\Collections\Selectable; + +/** + * @template T of object + * + * @implements PartialPaginatorInterface + * @implements \IteratorAggregate + */ +final class SelectablePartialPaginator implements \IteratorAggregate, PartialPaginatorInterface +{ + /** + * @var ReadableCollection + */ + private readonly ReadableCollection $slicedCollection; + + /** + * @param Selectable $selectable + */ + public function __construct( + readonly Selectable $selectable, + private readonly float $currentPage, + private readonly float $itemsPerPage + ) { + $criteria = Criteria::create() + ->setFirstResult((int) (($currentPage - 1) * $itemsPerPage)) + ->setMaxResults((int) $itemsPerPage); + + $this->slicedCollection = $selectable->matching($criteria); + } + + /** + * {@inheritdoc} + */ + public function getCurrentPage(): float + { + return $this->currentPage; + } + + /** + * {@inheritdoc} + */ + public function getItemsPerPage(): float + { + return $this->itemsPerPage; + } + + /** + * {@inheritdoc} + */ + public function count(): int + { + return $this->slicedCollection->count(); + } + + /** + * {@inheritdoc} + * + * @return \Traversable + */ + public function getIterator(): \Traversable + { + return $this->slicedCollection->getIterator(); + } +} diff --git a/src/Doctrine/Common/Tests/CollectionPaginatorTest.php b/src/Doctrine/Common/Tests/CollectionPaginatorTest.php new file mode 100644 index 00000000000..474dd6b4c5b --- /dev/null +++ b/src/Doctrine/Common/Tests/CollectionPaginatorTest.php @@ -0,0 +1,47 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Doctrine\Common\Tests; + +use ApiPlatform\Doctrine\Common\CollectionPaginator; +use Doctrine\Common\Collections\ArrayCollection; +use PHPUnit\Framework\TestCase; + +class CollectionPaginatorTest extends TestCase +{ + /** + * @dataProvider initializeProvider + */ + public function testInitialize($results, $currentPage, $itemsPerPage, $totalItems, $lastPage, $currentItems): void + { + $results = new ArrayCollection($results); + $paginator = new CollectionPaginator($results, $currentPage, $itemsPerPage); + + $this->assertSame((float) $currentPage, $paginator->getCurrentPage()); + $this->assertSame((float) $itemsPerPage, $paginator->getItemsPerPage()); + $this->assertSame((float) $totalItems, $paginator->getTotalItems()); + $this->assertCount($currentItems, $paginator); + $this->assertSame((float) $lastPage, $paginator->getLastPage()); + } + + public static function initializeProvider(): array + { + return [ + 'First of three pages of 3 items each' => [[0, 1, 2, 3, 4, 5, 6], 1, 3, 7, 3, 3], + 'Second of two pages of 3 items for the first page and 2 for the second' => [[0, 1, 2, 3, 4], 2, 3, 5, 2, 2], + 'Empty results' => [[], 1, 2, 0, 1, 0], + '0 items per page' => [[0, 1, 2, 3], 1, 0, 4, 1, 4], + 'Total items less than items per page' => [[0, 1, 2], 1, 4, 3, 1, 3], + ]; + } +} diff --git a/src/Doctrine/Common/Tests/SelectablePaginatorTest.php b/src/Doctrine/Common/Tests/SelectablePaginatorTest.php new file mode 100644 index 00000000000..9e1d37adc7c --- /dev/null +++ b/src/Doctrine/Common/Tests/SelectablePaginatorTest.php @@ -0,0 +1,47 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Doctrine\Common\Tests; + +use ApiPlatform\Doctrine\Common\SelectablePaginator; +use Doctrine\Common\Collections\ArrayCollection; +use PHPUnit\Framework\TestCase; + +class SelectablePaginatorTest extends TestCase +{ + /** + * @dataProvider initializeProvider + */ + public function testInitialize($results, $currentPage, $itemsPerPage, $totalItems, $lastPage, $currentItems): void + { + $results = new ArrayCollection($results); + $paginator = new SelectablePaginator($results, $currentPage, $itemsPerPage); + + $this->assertSame((float) $currentPage, $paginator->getCurrentPage()); + $this->assertSame((float) $itemsPerPage, $paginator->getItemsPerPage()); + $this->assertSame((float) $totalItems, $paginator->getTotalItems()); + $this->assertCount($currentItems, $paginator); + $this->assertSame((float) $lastPage, $paginator->getLastPage()); + } + + public static function initializeProvider(): array + { + return [ + 'First of three pages of 3 items each' => [[0, 1, 2, 3, 4, 5, 6], 1, 3, 7, 3, 3], + 'Second of two pages of 3 items for the first page and 2 for the second' => [[0, 1, 2, 3, 4], 2, 3, 5, 2, 2], + 'Empty results' => [[], 1, 2, 0, 1, 0], + '0 items per page' => [[0, 1, 2, 3], 1, 0, 4, 1, 4], + 'Total items less than items per page' => [[0, 1, 2], 1, 4, 3, 1, 3], + ]; + } +} diff --git a/src/Doctrine/Common/Tests/SelectablePartialPaginatorTest.php b/src/Doctrine/Common/Tests/SelectablePartialPaginatorTest.php new file mode 100644 index 00000000000..a71cb3ddb72 --- /dev/null +++ b/src/Doctrine/Common/Tests/SelectablePartialPaginatorTest.php @@ -0,0 +1,45 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Doctrine\Common\Tests; + +use ApiPlatform\Doctrine\Common\SelectablePartialPaginator; +use Doctrine\Common\Collections\ArrayCollection; +use PHPUnit\Framework\TestCase; + +class SelectablePartialPaginatorTest extends TestCase +{ + /** + * @dataProvider initializeProvider + */ + public function testInitialize($results, $currentPage, $itemsPerPage, $currentItems): void + { + $results = new ArrayCollection($results); + $paginator = new SelectablePartialPaginator($results, $currentPage, $itemsPerPage); + + $this->assertSame((float) $currentPage, $paginator->getCurrentPage()); + $this->assertSame((float) $itemsPerPage, $paginator->getItemsPerPage()); + $this->assertCount($currentItems, $paginator); + } + + public static function initializeProvider(): array + { + return [ + 'First of three pages of 3 items each' => [[0, 1, 2, 3, 4, 5, 6], 1, 3, 3], + 'Second of two pages of 3 items for the first page and 2 for the second' => [[0, 1, 2, 3, 4], 2, 3, 2], + 'Empty results' => [[], 1, 2, 0], + '0 items per page' => [[0, 1, 2, 3], 1, 0, 4], + 'Total items less than items per page' => [[0, 1, 2], 1, 4, 3], + ]; + } +}