分頁核心參數
- $totalRows:總資料筆數
- $pageSize:每頁顯示筆數
- $currentPage:目前頁碼(從 $_GET 取得)
- $totalPages:總頁數 =
ceil($totalRows / $pageSize)
- $offset:資料庫 OFFSET =
($currentPage - 1) * $pageSize
完整分頁類別實作
<?php
class Paginator {
private int $totalRows;
private int $pageSize;
private int $currentPage;
private int $totalPages;
private string $urlPattern; // 如 '/list?page={page}'
public function __construct(int $totalRows, int $pageSize = 20, string $urlPattern = '') {
$this->totalRows = max(0, $totalRows);
$this->pageSize = max(1, $pageSize);
$this->totalPages = (int)ceil($this->totalRows / $this->pageSize);
$this->currentPage = $this->normalizePage((int)($_GET['page'] ?? 1));
$this->urlPattern = $urlPattern ?: $this->detectUrlPattern();
}
private function normalizePage(int $page): int {
return max(1, min($page, $this->totalPages ?: 1));
}
private function detectUrlPattern(): string {
$base = rtrim($_SERVER['SCRIPT_NAME'], '/');
$query = $_GET;
unset($query['page']);
$qs = $query ? '?' . http_build_query($query) : '';
return $base . $qs . ($query ? '&' : '?') . 'page={page}';
}
public function getOffset(): int {
return ($this->currentPage - 1) * $this->pageSize;
}
public function getLimit(): string {
return "LIMIT {$this->getOffset()}, {$this->pageSize}";
}
public function getPageRange(int $displayCount = 9): array {
$half = (int)floor($displayCount / 2);
$start = max(1, $this->currentPage - $half);
$end = min($this->totalPages, $start + $displayCount - 1);
$start = max(1, $end - $displayCount + 1);
return range($start, $end);
}
public function render(): string {
if ($this->totalPages <= 1) return '';
$html = '<nav aria-label="Pagination"><ul class="pagination">';
// 首頁、上一頁
if ($this->currentPage > 1) {
$html .= $this->link(1, '« 首頁');
$html .= $this->link($this->currentPage - 1, '‹ 上一頁');
}
// 頁碼區間
foreach ($this->getPageRange() as $page) {
$html .= $page === $this->currentPage
? "<li class=\"active\"><span>{$page}</span></li>"
: $this->link($page, (string)$page);
}
// 下一頁、尾頁
if ($this->currentPage < $this->totalPages) {
$html .= $this->link($this->currentPage + 1, '下一頁 ›');
$html .= $this->link($this->totalPages, '尾頁 »');
}
$html .= '</ul></nav>';
return $html;
}
private function link(int $page, string $text): string {
$url = str_replace('{page}', $page, $this->urlPattern);
return "<li><a href=\"{$url}\">{$text}</a></li>";
}
// 取得分頁資訊陣列,供 API 回傳
public function toArray(): array {
return [
'current_page' => $this->currentPage,
'per_page' => $this->pageSize,
'total_pages' => $this->totalPages,
'total_rows' => $this->totalRows,
'offset' => $this->getOffset(),
];
}
}
使用範例
<?php
// 1. 取得總筆數
$pdo = new PDO($dsn, $user, $pass);
$total = $pdo->query("SELECT COUNT(*) FROM articles WHERE status=1")->fetchColumn();
// 2. 實例化分頁器
$pager = new Paginator($total, 15);
// 3. 查詢當頁資料
$stmt = $pdo->prepare("SELECT * FROM articles WHERE status=1 ORDER BY id DESC " . $pager->getLimit());
$stmt->execute();
$articles = $stmt->fetchAll(PDO::FETCH_ASSOC);
// 4. 輸出分頁導航
echo $pager->render();
// API 模式回傳 JSON
// echo json_encode(['data' => $articles, 'pagination' => $pager->toArray()]);
進階功能擴充
- 自訂 URL 結構:支援
/page/3、?p=3 等任意格式
- Ajax 分頁:
render() 改輸出 data-page 屬性,前端 JS 接管點擊
- SEO 優化:加入
rel="prev/next"、canonical 標籤
- 游標分頁:大資料量時用
WHERE id > :last_id LIMIT 20 替代 OFFSET
常見坑點提醒
- 總筆數查詢未加索引:
COUNT(*) 應有 WHERE 條件索引支援
- OFFSET 深分頁效能崩潰:百萬筆資料第 50000 頁會全表掃描,必須改用游標分頁
- 頁碼未驗證:惡意傳入
page=-999 或 page=999999 導致錯誤或資源浪費
- URL 參數遺失:分頁連結必須保留原本的搜尋、篩選參數