Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion docs/ja/MediaQuery.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,11 +107,12 @@ interface UserListInterface
}
```

実行結果の`Pages`は、配列アクセスでページを取得したり`count()`で全体の件数を取得することができます。
実行結果の`Pages`は、配列アクセスでページを、`count()`で全体の件数を、`getNbPages()`で総ページ数を取得できます。

```php
$userList = $injector->getInstance(UserListInterface::class);
$pages = $userList();
$page = $pages[1];
$total = count($pages);
$nbPages = $pages->getNbPages();
```
5 changes: 3 additions & 2 deletions docs/llms-full.txt
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ Every row of this table is a valid return type on a `#[DbQuery]` method.
| `AffectedRows` | UPDATE/DELETE | Exposes `->count` (int) and `->isAffected(): bool`. |
| `InsertedRow` | INSERT | Exposes `->values` (resolved params) and `->id` (?string lastInsertId). |
| custom `X implements PostQueryInterface` | SELECT or DML | Polymorphic dispatch — framework calls `X::fromContext($context)`. SELECT pre-hydrates rows on `$context->rows`; DML leaves `$context->rows = []`. |
| `Pages` (with `#[Pager]`) | SELECT | Lazy paginator. Index access executes `LIMIT`/`OFFSET`; `count()` does `COUNT`. |
| `Pages` (with `#[Pager]`) | SELECT | Lazy paginator. Index access executes `LIMIT`/`OFFSET`; `count()` does `COUNT`, and `getNbPages()` divides it by `perPage`. |

How dispatch is decided:

Expand Down Expand Up @@ -400,10 +400,11 @@ interface ProductRepository

$pages = $repo->list();
count($pages); // → COUNT query
$pages->getNbPages(); // → number of pages, from the same COUNT (memoized)
$page = $pages[1]; // → SELECT with LIMIT/OFFSET, rows hydrated to Product
$page->data; // items
$page->current; // int
$page->total; // int (total pages)
$page->total; // int (total item count)
$page->hasNext; // bool
$page->hasPrevious; // bool
(string) $page; // rendered pager HTML
Expand Down
5 changes: 3 additions & 2 deletions docs/reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -610,8 +610,9 @@ interface ProductRepository
}

$pages = $productRepo->getProducts();
$count = count($pages); // COUNT query を実行
$page = $pages[1]; // LIMIT/OFFSET 付き SELECT を実行
$count = count($pages); // COUNT query を実行
$nbPages = $pages->getNbPages(); // 総ページ数。件数を取ってceilする必要はない
$page = $pages[1]; // LIMIT/OFFSET 付き SELECT を実行

// Page object properties:
// $page->data // このページの item
Expand Down
2 changes: 1 addition & 1 deletion docs/tutorial/README.ja.md
Original file line number Diff line number Diff line change
Expand Up @@ -1333,7 +1333,7 @@ first stats row=MyBlog\ArticleStats commentCount=2 excerpt='Updated body.'

### 解説

- `count($pages)` は **総アイテム数** (= COUNT クエリの結果)。総ページ数ではないので注意。
- `count($pages)` は **総アイテム数** (= COUNT クエリの結果)。総ページ数は `$pages->getNbPages()` で取る (同じ COUNT を使い回す)。
- `$pages[1]` でページ1にアクセス → SELECT に LIMIT/OFFSET が付いて実行される (lazy)。
- `$page->data` は Article のリスト (`@return Pages<Article>` のおかげで hydration が効く)。
- `#[DbQuery(factory: ArticleStatsFactory::class)]` と `#[Pager]` を併用した場合、1.1 以降は `$page->data` の各行も `ArticleStatsFactory` で作られる。
Expand Down
2 changes: 1 addition & 1 deletion docs/tutorial/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1331,7 +1331,7 @@ first stats row=MyBlog\ArticleStats commentCount=2 excerpt='Updated body.'

### Explanation

- `count($pages)` returns the **total item count** from the COUNT query, not the number of pages.
- `count($pages)` returns the **total item count** from the COUNT query, not the number of pages. For the page count call `$pages->getNbPages()`, which reuses the same COUNT.
- `$pages[1]` accesses page 1 and lazily executes SELECT with LIMIT/OFFSET.
- `$page->data` contains a list of `Article` objects because of `@return Pages<Article>`.
- With `#[DbQuery(factory: ArticleStatsFactory::class)]` and `#[Pager]` together, Ray.MediaQuery 1.1+ builds each row in `$page->data` through `ArticleStatsFactory`.
Expand Down
12 changes: 12 additions & 0 deletions src/Exception/InvalidPerPageException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?php

declare(strict_types=1);

namespace Ray\MediaQuery\Exception;

/**
* PerPage must be a positive integer
*/
final class InvalidPerPageException extends LogicException
{
}
6 changes: 6 additions & 0 deletions src/MappedPages.php
Original file line number Diff line number Diff line change
Expand Up @@ -61,4 +61,10 @@ public function count(): int
{
return $this->pages->count();
}

#[Override]
public function getNbPages(): int
{
return $this->pages->getNbPages();
}
}
23 changes: 22 additions & 1 deletion src/Pages.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,26 @@
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
{
/** @var (callable(array<array-key, mixed>): 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<string, mixed> $params
* @param (callable(array<array-key, mixed>): mixed)|null $rowMapper
Expand All @@ -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;
}

Expand Down Expand Up @@ -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();
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

#[Override]
public function getNbPages(): int
{
return max(1, (int) ceil($this->count() / $this->perPage));
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
4 changes: 4 additions & 0 deletions src/PagesInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,8 @@
/** @extends ArrayAccess<int, mixed> */
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;
}
2 changes: 1 addition & 1 deletion src/SqlQuery.php
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, mixed> $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
Expand Down
6 changes: 6 additions & 0 deletions tests/Fake/FakeEmptyPages.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
6 changes: 6 additions & 0 deletions tests/Fake/FakePages.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,10 @@ public function count(): int
{
// TODO: Implement count() method.
}

public function getNbPages(): int
{
// TODO: Implement getNbPages() method.
return 0;
}
}
5 changes: 5 additions & 0 deletions tests/Fake/MappedPagesFakePages.php
Original file line number Diff line number Diff line change
Expand Up @@ -46,4 +46,9 @@ public function count(): int
{
return 1;
}

public function getNbPages(): int
{
return 1;
}
}
11 changes: 11 additions & 0 deletions tests/MappedPagesTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
63 changes: 63 additions & 0 deletions tests/PagesTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand All @@ -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());
}
}
32 changes: 32 additions & 0 deletions tests/SqlQueryTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,38 @@ public function testPagerCount(Pages $pages): void
$this->assertSame(2, count($pages));
}

/** @param Pages<mixed> $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']);
Expand Down
Loading