ページネーションの実装してみたその2

100件のアイテムを10個ずつ表示したら、1ページから何ページまで存在するでしょう、
またアイテムを0から数えて最後のページはいくつのアイテムが表示されているでしょう!

答え、最後は10ページ、10ページ目は90から99までのアイテムが表示されます。
もし10件を3個ずつ表示すると、全部で4ページ、最後のページは9が一つだけ存在します。

これを簡単に計算するクラスを作りました。

class Paginator
{
    public function __construct(
        private int $itemsCount,
        private int $pageSize
    )
    {
        if($itemsCount < 1 || $pageSize < 1)
        {
            throw new Exception();
        }
    }

    public function getItemsCount(): int
    {
        return $this->itemsCount;
    }

    public function getPageSize(): int
    {
        return $this->pageSize;
    }

    public function getLastPage(): int
    {
        return $this->getPage($this->itemsCount - 1);
    }

    public function getPage(int $position): int
    {
        $this->validPosition($position);
        return floor($position / $this->pageSize) + 1;
    }

    public function getRange(int $page): array
    {
        $this->validPage($page);

        $sp = ($page - 1) * $this->pageSize;
        $ep = $sp + $this->pageSize - 1;
        $ep = min($ep, $this->itemsCount - 1);
        $ep = max(0, $ep);
        return [$sp, $ep];
    }

    public function checkPosition(int $position): bool
    {
        return $position >= 0 && $position < $this->itemsCount;
    }

    public function validPosition(int $position): void
    {
        if(!$this->checkPosition($position))
        {
            throw new Exception('valid position');
        }
    }

    public function checkPage(int $page): bool
    {
        return $page <= $this->getLastPage() && $page >= 1;
    }

    public function validPage(int $page): void
    {
        if(!$this->checkPage($page))
        {
            throw new Exception('valid page');
        }
    }

}

function viewRange($itemsCount, $pageSize)
{
    echo "itemsCount = $itemsCount, pageSize = $pageSize\n";

    try
    {
        $p = new Paginator($itemsCount, $pageSize);

        // 最後のページを調べる
        $lastPage = $p->getLastPage();
        echo "最後のページは、 $lastPage\n";

        // 最後のページのインデックスを調べる
        list($begin, $end) = $p->getRange($lastPage);
        echo "最後のページのインデックスは、{$begin} から {$end} までです\n";
        echo "\n";
    }
    catch(Exception $ex)
    {
        echo " - \n";
    }
}

viewRange(100, 10);
viewRange(100, 9);
viewRange(100, 8);
viewRange(100, 3);
viewRange(100, 100);
viewRange(100, 33);
viewRange(0, 3);
viewRange(1, 3);
viewRange(2, 3);
viewRange(3, 3);
viewRange(4, 3);

viewRange(0, 1);
viewRange(1, 1);
viewRange(2, 1);
itemsCount = 100, pageSize = 10
最後のページは、 10
最後のページのインデックスは、90 から 99 までです

itemsCount = 100, pageSize = 9
最後のページは、 12
最後のページのインデックスは、99 から 99 までです

itemsCount = 100, pageSize = 8
最後のページは、 13
最後のページのインデックスは、96 から 99 までです

itemsCount = 100, pageSize = 3
最後のページは、 34
最後のページのインデックスは、99 から 99 までです

itemsCount = 100, pageSize = 100
最後のページは、 1
最後のページのインデックスは、0 から 99 までです

itemsCount = 100, pageSize = 33
最後のページは、 4
最後のページのインデックスは、99 から 99 までです

itemsCount = 0, pageSize = 3
 - 
itemsCount = 1, pageSize = 3
最後のページは、 1
最後のページのインデックスは、0 から 0 までです

itemsCount = 2, pageSize = 3
最後のページは、 1
最後のページのインデックスは、0 から 1 までです

itemsCount = 3, pageSize = 3
最後のページは、 1
最後のページのインデックスは、0 から 2 までです

itemsCount = 4, pageSize = 3
最後のページは、 2
最後のページのインデックスは、3 から 3 までです

itemsCount = 0, pageSize = 1
 - 
itemsCount = 1, pageSize = 1
最後のページは、 1
最後のページのインデックスは、0 から 0 までです

itemsCount = 2, pageSize = 1
最後のページは、 2
最後のページのインデックスは、1 から 1 までです

getLastPage()は最後のページを取得します。
getRange()は引数で渡されるページの範囲を取得します。

$p = new Paginator($itemsCount, $pageSize);

条件として、アイテム数が0、ページ当たりの表示件数が0の場合は例外を発生させます。
itemsCountが0の時はよくあることですが、計算上不都合なので例外を補足してアイテムが無いときの処理をします。

もうすこし詳しく見ていきます。

function viewRange($itemsCount, $pageSize)
{
    $p = new Paginator($itemsCount, $pageSize);

    echo "itemsCount = $itemsCount, pageSize = $pageSize\n";

    // 最後のページを調べる
    $lastPage = $p->getLastPage();

    for($i = 1; $i <= $lastPage; $i++)
    {
        list($begin, $end) = $p->getRange($i);
        echo "{$i}ページは、{$begin} から {$end} までです\n";
    }

    echo "\n";
}

viewRange(100, 8);

Paginatorクラスはそのままに、viewRange()メソッドをちょっと変えます。
100件のアイテムを8件ずつ表示した場合です。

結果は、

itemsCount = 100, pageSize = 8
1ページは、0 から 7 までです
2ページは、8 から 15 までです
3ページは、16 から 23 までです
4ページは、24 から 31 までです
5ページは、32 から 39 までです
6ページは、40 から 47 までです
7ページは、48 から 55 までです
8ページは、56 から 63 までです
9ページは、64 から 71 までです
10ページは、72 から 79 までです
11ページは、80 から 87 までです
12ページは、88 から 95 までです
13ページは、96 から 99 までです

これが出来ればページネーションが作成できます。
データベースで取得した結果をページネーションする時を考えます。

とあるSQLでmytableのレコード総数を取得します。

SELECT COUNT(*) FROM mytable;

仮に100件あったとしましょう。
8件ずつ表示するとしたら・・・。

1ページ目に必要なレコードの範囲は0から7までの8個です。
これをSQLにすると、次のSQLで最初の8件だけを取得できます。

SELECT * FROM mytable LIMIT 0, 8

2ページ目なら8から15までの8個

SELECT * FROM mytable LIMIT 8, 8

3ページ目なら、

SELECT * FROM mytable LIMIT 16, 8

といったように。

なおこのLIMIT句はMySQLやポスグレでは知られていますが、他のDB(OracleやSQL Server)には確かなかったはず。
ただORM類を使えば同じようなことが出来るのでそちらを使った方がいいでしょう。

ページはlist.php?page=3のようにURLのパラメータに載せてあげればいいだけです。
pageが1から13までの範囲では例外なくいくはずです。

さらにviewRange()をちょっと変えて

function viewRange($itemsCount, $pageSize, $index)
{
    $p = new Paginator($itemsCount, $pageSize);

    $page = $p->getPage($index);
    list($begin, $end) = $p->getRange($page);

    echo "インデックス ${index} は、{$page}ページの{$begin}-{$end}に含まれます。";
    echo "\n";
}

viewRange(100, 8, 54);
viewRange(100, 8, 55);
viewRange(100, 8, 56);
インデックス 54 は、7ページの48-55に含まれます。
インデックス 55 は、7ページの48-55に含まれます。
インデックス 56 は、8ページの56-63に含まれます。

getPage()はインデックスが何ページ目にあるかを調べます。
あくまでインデックスでを元にしていて、主キーがどの位置にあるのかを表しているわけではないので
SQL側でごにょごにょしないとあまり役には立たないと思います。

ページネーションのナビゲーターというかリンクの作成は次のページで書来ました。

https://kurage-worker.com/2022/pagination

certificate Docker Gutenberg Hyper-V openssl PHP React ReduxToolkit REST ubuntu WordPress オレオレ認証局 フレームワーク