diff --git a/src/Exception/InvalidPerPageException.php b/src/Exception/InvalidPerPageException.php new file mode 100644 index 0000000..c104970 --- /dev/null +++ b/src/Exception/InvalidPerPageException.php @@ -0,0 +1,12 @@ +pages->count(); } + + #[Override] + public function getNbPages(): int + { + return $this->pages->getNbPages(); + } } diff --git a/src/Pages.php b/src/Pages.php index 4f820e4..f18cca7 100644 --- a/src/Pages.php +++ b/src/Pages.php @@ -9,9 +9,12 @@ use Ray\AuraSqlModule\Pagerfanta\AuraSqlPagerInterface; use Ray\AuraSqlModule\Pagerfanta\ExtendedPdoAdapter; use Ray\AuraSqlModule\Pagerfanta\Page; +use Ray\MediaQuery\Exception\InvalidPerPageException; use Ray\MediaQuery\Exception\LogicException; +use function ceil; use function is_array; +use function max; /** @template T of class-string|mixed */ final class Pages implements PagesInterface @@ -19,6 +22,13 @@ final class Pages implements PagesInterface /** @var (callable(array): mixed)|null */ private $rowMapper; + /** + * Memoized result count, shared by count() and getNbPages() as in Pagerfanta + * + * @var int<0, max>|null + */ + private int|null $nbResults = null; + /** * @param array $params * @param (callable(array): mixed)|null $rowMapper @@ -28,8 +38,13 @@ public function __construct( private ExtendedPdoInterface $pdo, private string $sql, private array $params, + private int $perPage, callable|null $rowMapper = null, ) { + if ($this->perPage < 1) { + throw new InvalidPerPageException((string) $this->perPage); + } + $this->rowMapper = $rowMapper; } @@ -93,6 +108,12 @@ public function offsetUnset(mixed $offset): void #[Override] public function count(): int { - return (new ExtendedPdoAdapter($this->pdo, $this->sql, $this->params))->getNbResults(); + return $this->nbResults ??= (new ExtendedPdoAdapter($this->pdo, $this->sql, $this->params))->getNbResults(); + } + + #[Override] + public function getNbPages(): int + { + return max(1, (int) ceil($this->count() / $this->perPage)); } } diff --git a/src/PagesInterface.php b/src/PagesInterface.php index 3716bc4..762f5b2 100644 --- a/src/PagesInterface.php +++ b/src/PagesInterface.php @@ -10,4 +10,8 @@ /** @extends ArrayAccess */ interface PagesInterface extends ArrayAccess, Countable { + /** + * Returns the total number of pages (ceil of result count / perPage, minimum 1 as in Pagerfanta). + */ + public function getNbPages(): int; } diff --git a/src/SqlQuery.php b/src/SqlQuery.php index 0bb8f39..5c42ea3 100644 --- a/src/SqlQuery.php +++ b/src/SqlQuery.php @@ -236,7 +236,7 @@ public function getPages(string $sqlId, array $values, int $perPage, string $que $pager = $this->pagerFactory->newInstance($this->pdo, $this->getSql($sqlId), $values, $perPage, $queryTemplate, $entity); /** @var array $values */ - return new Pages($pager, $this->pdo, $this->getSql($sqlId), $values); + return new Pages($pager, $this->pdo, $this->getSql($sqlId), $values, $perPage); } private function getSql(string $sqlId): string diff --git a/tests/Fake/FakeEmptyPages.php b/tests/Fake/FakeEmptyPages.php index c814381..b8eca3f 100644 --- a/tests/Fake/FakeEmptyPages.php +++ b/tests/Fake/FakeEmptyPages.php @@ -40,4 +40,10 @@ public function count(): int { return $this->iter->count(); } + + public function getNbPages(): int + { + // Empty result set is still one (empty) page, as in Pagerfanta + return 1; + } } diff --git a/tests/Fake/FakePages.php b/tests/Fake/FakePages.php index 6c3bdda..edffb89 100644 --- a/tests/Fake/FakePages.php +++ b/tests/Fake/FakePages.php @@ -30,4 +30,10 @@ public function count(): int { // TODO: Implement count() method. } + + public function getNbPages(): int + { + // TODO: Implement getNbPages() method. + return 0; + } } diff --git a/tests/Fake/MappedPagesFakePages.php b/tests/Fake/MappedPagesFakePages.php index ea1d5a4..74b2833 100644 --- a/tests/Fake/MappedPagesFakePages.php +++ b/tests/Fake/MappedPagesFakePages.php @@ -46,4 +46,9 @@ public function count(): int { return 1; } + + public function getNbPages(): int + { + return 1; + } } diff --git a/tests/MappedPagesTest.php b/tests/MappedPagesTest.php index 66122a5..a155a9e 100644 --- a/tests/MappedPagesTest.php +++ b/tests/MappedPagesTest.php @@ -62,6 +62,17 @@ public function testDelegatesMutationAndCount(): void $this->assertSame(1, $pages->count()); } + public function testDelegatesGetNbPages(): void + { + $delegate = $this->pages($this->page([])); + $pages = new MappedPages( + $delegate, + static fn (array $row): array => $row, + ); + + $this->assertSame(1, $pages->getNbPages()); + } + private function page(mixed $data): Page { $reflection = new ReflectionClass(Page::class); diff --git a/tests/PagesTest.php b/tests/PagesTest.php index 51fa1c1..4e26094 100644 --- a/tests/PagesTest.php +++ b/tests/PagesTest.php @@ -7,6 +7,7 @@ use Aura\Sql\ExtendedPdoInterface; use PHPUnit\Framework\TestCase; use Ray\AuraSqlModule\Pagerfanta\AuraSqlPagerInterface; +use Ray\MediaQuery\Exception\InvalidPerPageException; final class PagesTest extends TestCase { @@ -20,9 +21,71 @@ public function testOffsetGetReturnsNullWhenDelegateHasNoPage(): void $this->createStub(ExtendedPdoInterface::class), 'SELECT 1', [], + 10, ); $this->assertFalse(isset($pages[3])); $this->assertNull($pages[3]); } + + public function testConstructorRejectsZeroPerPage(): void + { + $this->expectException(InvalidPerPageException::class); + + new Pages( + $this->createStub(AuraSqlPagerInterface::class), + $this->createStub(ExtendedPdoInterface::class), + 'SELECT 1', + [], + 0, + ); + } + + public function testConstructorRejectsNegativePerPage(): void + { + $this->expectException(InvalidPerPageException::class); + + new Pages( + $this->createStub(AuraSqlPagerInterface::class), + $this->createStub(ExtendedPdoInterface::class), + 'SELECT 1', + [], + -1, + ); + } + + public function testGetNbPagesReturnsOneWhenEmpty(): void + { + $pdo = $this->createStub(ExtendedPdoInterface::class); + $pdo->method('fetchValue')->willReturn('0'); + + $pages = new Pages( + $this->createStub(AuraSqlPagerInterface::class), + $pdo, + 'SELECT * FROM todo', + [], + 10, + ); + + $this->assertSame(0, $pages->count()); + $this->assertSame(1, $pages->getNbPages()); + } + + public function testCountQueryRunsOnlyOnce(): void + { + $pdo = $this->createMock(ExtendedPdoInterface::class); + $pdo->expects($this->once())->method('fetchValue')->willReturn('3'); + + $pages = new Pages( + $this->createStub(AuraSqlPagerInterface::class), + $pdo, + 'SELECT * FROM todo', + [], + 2, + ); + + $this->assertSame(3, $pages->count()); + $this->assertSame(2, $pages->getNbPages()); + $this->assertSame(2, $pages->getNbPages()); + } } diff --git a/tests/SqlQueryTest.php b/tests/SqlQueryTest.php index 9774e76..79d4baa 100644 --- a/tests/SqlQueryTest.php +++ b/tests/SqlQueryTest.php @@ -108,6 +108,38 @@ public function testPagerCount(Pages $pages): void $this->assertSame(2, count($pages)); } + /** @param Pages $pages */ + #[Depends('testPager')] + public function testPagerNbPages(Pages $pages): void + { + $this->assertSame(2, $pages->getNbPages()); + } + + public function testPagerNbPagesWithPerPage2(): void + { + $this->sqlQuery->exec('todo_add', ['id' => '2', 'title' => 'walk']); + $pages = $this->sqlQuery->getPages('todo_list', [], 2); + + $this->assertSame(1, $pages->getNbPages()); + } + + public function testPagerNbPagesRoundsUp(): void + { + $this->sqlQuery->exec('todo_add', ['id' => '2', 'title' => 'walk']); + $this->sqlQuery->exec('todo_add', ['id' => '3', 'title' => 'sleep']); + $pages = $this->sqlQuery->getPages('todo_list', [], 2); + + $this->assertSame(2, $pages->getNbPages()); + } + + public function testPagerNbPagesMinimumOneWhenEmpty(): void + { + $pages = $this->sqlQuery->getPages('todo_item', ['id' => '__none__'], 10); + + $this->assertSame(0, count($pages)); + $this->assertSame(1, $pages->getNbPages()); + } + public function testCount(): void { $this->sqlQuery->exec('todo_add', ['id' => '2', 'title' => 'walk']);