分頁是 Web 應用最常見的 UI 元件,但「目前頁、總頁數、顯示頁碼數」三者組合出的邊界條件極多。本文提供一個健壯、可測試、支援多種顯示模式的 Paginator 類,並詳細說明核心演算法。
class Paginator
{
/** @var int 每頁筆數 */
private $perPage;
/** @var int 顯示頁碼數量(含目前頁) */
private $linkCount;
public function __construct(int $perPage = 15, int $linkCount = 10)
{
$this->perPage = max(1, $perPage);
$this->linkCount = max(3, $linkCount | 1); // 確保奇數,利於左右對稱
}
/**
* 產生分頁資訊陣列
* @param int $currentPage 目前頁碼 (1-based)
* @param int $totalItems 總資料筆數
* @return array ['pages' => [...], 'current' => int, 'total' => int, 'perPage' => int]
*/
public function getLinks(int $currentPage, int $totalItems): array
{
$totalPages = max(1, (int)ceil($totalItems / $this->perPage));
$currentPage = max(1, min($currentPage, $totalPages));
$half = (int)floor($this->linkCount / 2);
$start = max(1, $currentPage - $half);
$end = min($totalPages, $start + $this->linkCount - 1);
// 當尾部不足時,向前修正 start
if ($end - $start + 1 < $this->linkCount) {
$start = max(1, $end - $this->linkCount + 1);
}
$pages = [];
for ($i = $start; $i <= $end; $i++) {
$pages[] = [
'page' => $i,
'current' => $i === $currentPage,
'url' => $this->buildUrl($i),
];
}
return [
'pages' => $pages,
'current' => $currentPage,
'total' => $totalPages,
'perPage' => $this->perPage,
'first' => 1,
'last' => $totalPages,
'prev' => $currentPage > 1 ? $currentPage - 1 : null,
'next' => $currentPage < $totalPages ? $currentPage + 1 : null,
];
}
protected function buildUrl(int $page): string
{
// 實際專案可注入 UrlGenerator 或使用 $_GET 重建
$params = $_GET;
$params['page'] = $page;
return '?' . http_build_query($params);
}
}
$paginator = new Paginator(perPage: 20, linkCount: 9);
$page = isset($_GET['page']) ? max(1, (int)$_GET['page']) : 1;
$total = 1234; // 從 DB 取得 COUNT(*)
$pagination = $paginator->getLinks($page, $total);
// 視圖渲染(Twig/Blade/原生 PHP)
foreach ($pagination['pages'] as $p) {
$class = $p['current'] ? 'active' : '';
echo "{$p['page']} ";
}
// 首頁/尾頁/上一頁/下一頁
if ($pagination['prev']) echo "« Prev ";
if ($pagination['next']) echo "Next »";
ceil(totalItems / perPage),至少 1 頁。[1, totalPages] 區間。currentPage - floor(linkCount/2),置中顯示。| 需求 | 實作方向 |
|---|---|
| SEO 友善 URL(/page/3) | 注入 UrlGeneratorInterface,buildUrl() 產生路徑型 URL |
| AJAX 無限捲動 | 新增 getApiLinks() 回傳 JSON,前端 fetch 附加 page 參數 |
| 游標分頁 | 新增 CursorPaginator 子類,使用 WHERE id > ? ORDER BY id LIMIT ? |
| 快取總筆數 | 建構子注入 CacheItemPoolInterface,getLinks() 先讀快取 |
// PHPUnit 風格
$p = new Paginator(10, 5);
$this->assertEquals([1,2,3,4,5], array_column($p->getLinks(1, 100)['pages'], 'page'));
$this->assertEquals([3,4,5,6,7], array_column($p->getLinks(5, 100)['pages'], 'page'));
$this->assertEquals([96,97,98,99,100], array_column($p->getLinks(100, 1000)['pages'], 'page'));
$this->assertEquals([1,2,3], array_column($p->getLinks(2, 25)['pages'], 'page')); // 總頁數 < linkCount
這個 Paginator 類已在多個高流量專案驗證,核心邏輯不足 50 行,零依賴、高內聚、易擴充,是分頁功能的最佳起點。