分頁本質是對資料集進行**切片**。關鍵參數:
$total:總筆數$perPage:每頁筆數$currentPage:當前頁碼 (從 1 開始)$totalPages = ceil($total / $perPage)$offset = ($currentPage - 1) * $perPage$limit = $perPage<?php
class Paginator
{
private int $total;
private int $perPage;
private int $currentPage;
private string $baseUrl;
private array $queryParams;
public function __construct(
int $total,
int $perPage = 20,
int $currentPage = 1,
string $baseUrl = '',
array $queryParams = []
) {
$this->total = max(0, $total);
$this->perPage = max(1, $perPage);
$this->currentPage = max(1, min($currentPage, $this->getTotalPages()));
$this->baseUrl = $baseUrl ?: (isset($_SERVER['REQUEST_URI']) ? $_SERVER['REQUEST_URI'] : '/');
$this->queryParams = $queryParams;
}
public function getTotalPages(): int
{
return (int)ceil($this->total / $this->perPage);
}
public function getOffset(): int
{
return ($this->currentPage - 1) * $this->perPage;
}
public function getLimit(): int
{
return $this->perPage;
}
public function getSqlLimit(): string
{
return "LIMIT {$this->getOffset()}, {$this->perPage}";
}
public function getPageInfo(): array
{
return [
'total' => $this->total,
'per_page' => $this->perPage,
'current_page' => $this->currentPage,
'total_pages' => $this->getTotalPages(),
'offset' => $this->getOffset(),
'from' => $this->total > 0 ? $this->getOffset() + 1 : 0,
'to' => min($this->getOffset() + $this->perPage, $this->total),
];
}
// 建構分頁連結 URL
private function buildUrl(int $page): string
{
$params = array_merge($this->queryParams, ['page' => $page]);
$parsed = parse_url($this->baseUrl);
$path = $parsed['path'] ?? '/';
$query = http_build_query($params, '', '&', PHP_QUERY_RFC3986);
return $path . ($query ? '?' . $query : '');
}
// 產生頁碼範圍 (如 1 ... 4 5 [6] 7 8 ... 20)
public function getPageRange(int $window = 2): array
{
$totalPages = $this->getTotalPages();
$current = $this->currentPage;
$range = [];
$start = max(1, $current - $window);
$end = min($totalPages, $current + $window);
// 總是顯示首尾頁
if ($start > 1) $range[] = 1;
if ($start > 2) $range[] = '...';
for ($i = $start; $i <= $end; $i++) {
$range[] = $i;
}
if ($end < $totalPages - 1) $range[] = '...';
if ($end < $totalPages) $range[] = $totalPages;
return $range;
}
public function renderBootstrap5(): string
{
$totalPages = $this->getTotalPages();
if ($totalPages <= 1) return '';
$html = '';
return $html;
}
public function renderApi(): array
{
return [
'pagination' => $this->getPageInfo(),
'links' => [
'first' => $this->buildUrl(1),
'last' => $this->buildUrl($this->getTotalPages()),
'prev' => $this->currentPage > 1 ? $this->buildUrl($this->currentPage - 1) : null,
'next' => $this->currentPage < $this->getTotalPages() ? $this->buildUrl($this->currentPage + 1) : null,
],
];
}
}
?>
<?php
function paginate(int $total, int $perPage = 20, int $page = 1): array
{
$total = max(0, $total);
$perPage = max(1, $perPage);
$totalPages = (int)ceil($total / $perPage);
$page = max(1, min($page, $totalPages));
$offset = ($page - 1) * $perPage;
return [
'total' => $total,
'per_page' => $perPage,
'current' => $page,
'total_pages' => $totalPages,
'offset' => $offset,
'limit' => $perPage,
'sql_limit' => "LIMIT $offset, $perPage",
'from' => $total > 0 ? $offset + 1 : 0,
'to' => min($offset + $perPage, $total),
];
}
function renderPagination(array $pager, string $baseUrl = ''): string
{
if ($pager['total_pages'] <= 1) return '';
$baseUrl = $baseUrl ?: ($_SERVER['REQUEST_URI'] ?? '/');
$html = '';
$range = range(1, $pager['total_pages']); // 簡化:顯示所有頁碼
// 上一頁
if ($pager['current'] > 1) {
$html .= '« 上一頁 ';
}
foreach ($range as $p) {
if ($p == $pager['current']) {
$html .= '' . $p . ' ';
} else {
$html .= '' . $p . ' ';
}
}
// 下一頁
if ($pager['current'] < $pager['total_pages']) {
$html .= '下一頁 »';
}
$html .= '';
return $html;
}
function buildPageUrl(string $baseUrl, int $page): string
{
$parsed = parse_url($baseUrl);
$query = $parsed['query'] ?? '';
parse_str($query, $params);
$params['page'] = $page;
$newQuery = http_build_query($params, '', '&', PHP_QUERY_RFC3986);
return ($parsed['path'] ?? '/') . ($newQuery ? '?' . $newQuery : '');
}
?>
<?php
interface PaginatorDataSource
{
public function getTotal(): int;
public function getSlice(int $offset, int $limit): array;
}
class ArrayDataSource implements PaginatorDataSource
{
private array $data;
public function __construct(array $data) { $this->data = $data; }
public function getTotal(): int { return count($this->data); }
public function getSlice(int $offset, int $limit): array {
return array_slice($this->data, $offset, $limit);
}
}
class PdoDataSource implements PaginatorDataSource
{
private PDO $pdo;
private string $countSql;
private string $selectSql;
private array $params;
public function __construct(PDO $pdo, string $countSql, string $selectSql, array $params = [])
{
$this->pdo = $pdo;
$this->countSql = $countSql;
$this->selectSql = $selectSql;
$this->params = $params;
}
public function getTotal(): int
{
$stmt = $this->pdo->prepare($this->countSql);
$stmt->execute($this->params);
return (int)$stmt->fetchColumn();
}
public function getSlice(int $offset, int $limit): array
{
$sql = $this->selectSql . " LIMIT $offset, $limit";
$stmt = $this->pdo->prepare($sql);
$stmt->execute($this->params);
return $stmt->fetchAll(PDO::FETCH_ASSOC);
}
}
// 統一分頁服務
class PaginationService
{
public function paginate(PaginatorDataSource $source, int $perPage, int $page): array
{
$total = $source->getTotal();
$pager = paginate($total, $perPage, $page); // 使用方案二的函數
$items = $source->getSlice($pager['offset'], $pager['limit']);
return ['pager' => $pager, 'items' => $items];
}
}
// 使用
$source = new PdoDataSource($pdo, 'SELECT COUNT(*) FROM users', 'SELECT * FROM users ORDER BY id DESC');
$result = (new PaginationService())->paginate($source, 20, $_GET['page'] ?? 1);
$users = $result['items'];
$pager = $result['pager'];
?>
| 場景 | 優化策略 |
|---|---|
| MyISAM / 有觸發器維護計數 | SELECT table_rows FROM information_schema.tables (近似值) |
| InnoDB 大表 | 快取總數 (Redis),異步更新;或維覽表/欄位記錄總數 |
| 複雜 WHERE 條件 | 建立覆蓋索引;或使用 SQL_CALC_FOUND_ROWS + FOUND_ROWS() (MySQL 8.0 棄用) |
| 游標分頁 | 不使用 OFFSET,改用 WHERE id > $lastId ORDER BY id LIMIT 20 (適合無限滾動) |
<?php
function cursorPaginate(PDO $pdo, string $sql, array $params, int $limit, ?int $cursor = null): array
{
// $sql 應為不含 LIMIT 的基礎查詢,且有確定排序 (如 ORDER BY id ASC)
if ($cursor !== null) {
$sql .= " AND id > :cursor";
$params['cursor'] = $cursor;
}
$sql .= " LIMIT " . ($limit + 1); // 多取一筆判斷是否有下一頁
$stmt = $pdo->prepare($sql);
$stmt->execute($params);
$items = $stmt->fetchAll(PDO::FETCH_ASSOC);
$hasMore = count($items) > $limit;
if ($hasMore) {
array_pop($items); // 移除多取的那筆
$nextCursor = end($items)['id'] ?? null;
} else {
$nextCursor = null;
}
return [
'items' => $items,
'next_cursor' => $nextCursor,
'has_more' => $hasMore,
];
}
?>
<script>
// 無限滾動範例 (游標分頁)
let cursor = null;
let loading = false;
async function loadMore() {
if (loading) return;
loading = true;
const url = new URL('/api/items', location.origin);
if (cursor) url.searchParams.set('cursor', cursor);
const res = await fetch(url);
const data = await res.json();
data.items.forEach(item => appendItem(item));
cursor = data.next_cursor;
loading = false;
if (!data.has_more) window.removeEventListener('scroll', onScroll);
}
function onScroll() {
if (window.innerHeight + window.scrollY >= document.body.offsetHeight - 200) {
loadMore();
}
}
window.addEventListener('scroll', onScroll);
loadMore();
</script>
LengthAwarePaginator、CursorPaginator,Model::paginate(20) 一行搞定。KnpPaginatorBundle、Doctrine ORM Paginator。paginate() 鏈式呼叫。league/plates 模板 + 上方 Paginator 類,或 illuminate/pagination (Laravel 分頁元件獨立套件)。LIMIT 1000000, 20 需掃描 100 萬行)。