首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> 網頁程式設計 >> PHP 程式設計 >> PHP 綜合知識

PHP 分頁類完整實作:三種經典架構與現代最佳實踐

     編輯:PHP 綜合知識

分頁核心邏輯

分頁本質是對資料集進行**切片**。關鍵參數:

  • $total:總筆數
  • $perPage:每頁筆數
  • $currentPage:當前頁碼 (從 1 開始)
  • $totalPages = ceil($total / $perPage)
  • $offset = ($currentPage - 1) * $perPage
  • $limit = $perPage

方案一:物件導向完整分頁類 (適合傳統 MVC)

<?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 = '';
    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 : '');
}
?>

方案三:資料來源抽象化 (支援陣列、資料庫、API 統一介面)

<?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'];
?>

資料庫層面優化:避免 COUNT(*) 全表掃描

場景優化策略
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 (適合無限滾動)

游標分頁 (Cursor Pagination) 實作

<?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,
    ];
}
?>

前端整合:AJAX 無限滾動 / 載入更多

<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>

現代框架整合

  • Laravel:內建 LengthAwarePaginator、CursorPaginator,Model::paginate(20) 一行搞定。
  • Symfony:KnpPaginatorBundle、Doctrine ORM Paginator。
  • ThinkPHP:paginate() 鏈式呼叫。
  • 純 PHP/微框架:引入 league/plates 模板 + 上方 Paginator 類,或 illuminate/pagination (Laravel 分頁元件獨立套件)。

總結

  • OFFSET 分頁:簡單、支援跳頁,但大偏移量效能差 (LIMIT 1000000, 20 需掃描 100 萬行)。
  • 游標分頁:效能穩定、適合無限滾動,但不支援隨機跳頁、需唯一有序欄位 (通常是 ID)。
  • **架構建議**:抽象資料來源介面,分頁邏輯與資料存取解耦,便於測試與切換後端 (MySQL/PostgreSQL/Elasticsearch/API)。
  • 前端趨勢:無限滾動 (游標) + 傳統分頁 (OFFSET) 並存,視 UX 需求選擇。
  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved