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

PHP 分頁導航元件:可重用類別、URL 保持參數、Bootstrap/Tailwind 樣式支援

     編輯:PHP 相關知識

分頁是列表頁面核心元件。本文提供功能完整的 PHP 分頁類別,支援參數保持、多種樣式框架整合、AJAX 分頁、SEO 友善結構與單元測試。

核心分頁類別

<?php
class Paginator {
    public int $currentPage = 1;
    public int $perPage = 20;
    public int $totalItems = 0;
    public string $baseUrl = '';
    public array $queryParams = [];
    public int $visiblePages = 5; // 顯示頁碼數量
    public string $parameterName = 'page';

    // 模板可自訂
    public string $prevText = '« 上一頁';
    public string $nextText = '下一頁 »';
    public string $firstText = '首頁';
    public string $lastText = '末頁';
    public string $ellipsis = '…';

    public function __construct(array $config = []) {
        foreach ($config as $key => $value) {
            if (property_exists($this, $key)) {
                $this->$key = $value;
            }
        }
        
        // 自動偵測當前頁
        if (empty($this->baseUrl)) {
            $this->baseUrl = (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? 'https' : 'http') 
                . '://' . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'];
        }
        $this->currentPage = max(1, (int)($_GET[$this->parameterName] ?? 1));
        $this->queryParams = $_GET;
        unset($this->queryParams[$this->parameterName]);
    }

    public function setTotalItems(int $total): self {
        $this->totalItems = max(0, $total);
        return $this;
    }

    public function getTotalPages(): int {
        return max(1, (int)ceil($this->totalItems / $this->perPage));
    }

    public function getOffset(): int {
        return ($this->currentPage - 1) * $this->perPage;
    }

    public function getLimit(): int {
        return $this->perPage;
    }

    public function getPageRange(): array {
        $totalPages = $this->getTotalPages();
        $half = (int)floor($this->visiblePages / 2);
        $start = max(1, $this->currentPage - $half);
        $end = min($totalPages, $start + $this->visiblePages - 1);
        
        // 調整 start 以填滿 visiblePages
        $start = max(1, $end - $this->visiblePages + 1);
        
        return range($start, $end);
    }

    public function buildUrl(int $page): string {
        $params = $this->queryParams;
        $params[$this->parameterName] = $page;
        $separator = str_contains($this->baseUrl, '?') ? '&' : '?';
        return $this->baseUrl . $separator . http_build_query($params);
    }

    // 資料庫查詢輔助
    public function applyToQuery(PDOStatement|mysqli_stmt $stmt): void {
        $stmt->execute(array_merge($stmt->boundParams ?? [], [
            ':offset' => $this->getOffset(),
            ':limit' => $this->getLimit()
        ]));
    }

    // 陣列分頁(小資料集)
    public function paginateArray(array $items): array {
        return array_slice($items, $this->getOffset(), $this->getLimit());
    }
}
?>

視圖渲染器(支援多種框架)

<?php
interface PaginatorRenderer {
    public function render(Paginator $paginator): string;
}

class Bootstrap5Renderer implements PaginatorRenderer {
    public function render(Paginator $p): string {
        $totalPages = $p->getTotalPages();
        if ($totalPages <= 1) return '';

        $html = '';
        
        // 資訊列
        $start = $p->totalItems ? $p->getOffset() + 1 : 0;
        $end = min($p->getOffset() + $p->perPage, $p->totalItems);
        $html .= "
"; $html .= "顯示 {$start} - {$end} 筆,共 {$p->totalItems} 筆(第 {$p->currentPage} / {$totalPages} 頁)"; $html .= "
"; return $html; } private function pageItem(string $text, int $page, bool $disabled, Paginator $p, bool $isNumber = false): string { $attrs = ['class' => 'page-link']; if ($disabled) $attrs['class'] .= ' disabled'; if ($isNumber && $page === $p->currentPage) { $attrs['class'] .= ' active'; $attrs['aria-current'] = 'page'; } $href = $disabled ? '#' : $p->buildUrl($page); $attrStr = ' ' . implode(' ', array_map(fn($k, $v) => "$k=\"$v\"", array_keys($attrs), $attrs)); return "
  • currentPage ? ' active' : '') . "'>" . "{$text}" . "
  • "; } } class TailwindRenderer implements PaginatorRenderer { public function render(Paginator $p): string { $totalPages = $p->getTotalPages(); if ($totalPages <= 1) return ''; $html = ''; $start = $p->totalItems ? $p->getOffset() + 1 : 0; $end = min($p->getOffset() + $p->perPage, $p->totalItems); $html .= "

    "; $html .= "顯示 {$start} - {$end} 筆,共 {$p->totalItems} 筆"; $html .= "

    "; return $html; } } ?>

    使用範例

    基本用法(PDO)

    <?php
    // 控制器
    $paginator = new Paginator([
        'perPage' => 15,
        'parameterName' => 'p'
    ]);
    
    // 取得總數
    $stmt = $pdo->query("SELECT COUNT(*) FROM articles WHERE status = 'published'");
    $paginator->setTotalItems((int)$stmt->fetchColumn());
    
    // 查詢資料
    $stmt = $pdo->prepare("SELECT * FROM articles WHERE status = 'published' ORDER BY created_at DESC LIMIT :limit OFFSET :offset");
    $stmt->bindValue(':limit', $paginator->getLimit(), PDO::PARAM_INT);
    $stmt->bindValue(':offset', $paginator->getOffset(), PDO::PARAM_INT);
    $stmt->execute();
    $articles = $stmt->fetchAll();
    
    // 視圖
    $renderer = new Bootstrap5Renderer();
    echo $renderer->render($paginator);
    ?>

    Laravel 風格靜態工廠

    <?php
    class Paginate {
        public static function make(int $total, int $perPage = 20, array $options = []): Paginator {
            $paginator = new Paginator(array_merge([
                'perPage' => $perPage,
                'baseUrl' => url()->current(), // Laravel helper
                'queryParams' => request()->except('page')
            ], $options));
            $paginator->setTotalItems($total);
            return $paginator;
        }
    }
    
    // 使用
    $articles = Article::query()->latest()->paginate(15);
    // 視圖直接用 $articles->links()(Laravel 內建)
    ?>

    AJAX 無限捲動 / 載入更多

    後端 API

    <?php
    // api/articles.php
    $page = max(1, (int)($_GET['page'] ?? 1));
    $perPage = 10;
    $offset = ($page - 1) * $perPage;
    
    $stmt = $pdo->prepare("SELECT * FROM articles WHERE status = 'published' ORDER BY created_at DESC LIMIT ? OFFSET ?");
    $stmt->execute([$perPage, $offset]);
    $articles = $stmt->fetchAll();
    
    $totalStmt = $pdo->query("SELECT COUNT(*) FROM articles WHERE status = 'published'");
    $total = (int)$totalStmt->fetchColumn();
    
    header('Content-Type: application/json');
    echo json_encode([
        'data' => $articles,
        'pagination' => [
            'current_page' => $page,
            'per_page' => $perPage,
            'total' => $total,
            'last_page' => (int)ceil($total / $perPage),
            'has_more' => $page * $perPage < $total
        ]
    ]);
    ?>

    前端整合

    <div id="articles-container"></div>
    <button id="load-more" class="btn btn-primary" style="display:none">載入更多</button>
    
    <script>
    let currentPage = 1;
    let loading = false;
    
    async function loadArticles(append = true) {
        if (loading) return;
        loading = true;
        document.getElementById('load-more').textContent = '載入中...';
        
        try {
            const res = await fetch(`/api/articles.php?page=${currentPage}`);
            const {data, pagination} = await res.json();
            
            const container = document.getElementById('articles-container');
            if (append) {
                data.forEach(article => container.insertAdjacentHTML('beforeend', renderArticle(article)));
            } else {
                container.innerHTML = data.map(renderArticle).join('');
            }
            
            if (pagination.has_more) {
                currentPage++;
                document.getElementById('load-more').style.display = 'block';
                document.getElementById('load-more').textContent = '載入更多';
            } else {
                document.getElementById('load-more').style.display = 'none';
            }
        } catch (e) {
            console.error(e);
        } finally {
            loading = false;
        }
    }
    
    function renderArticle(article) {
        return `<article class="card mb-3"><div class="card-body"><h5>${escapeHtml(article.title)}</h5><p>${escapeHtml(article.excerpt)}</p></div></article>`;
    }
    
    document.getElementById('load-more').addEventListener('click', () => loadArticles());
    loadArticles(false); // 初始載入
    </script>

    SEO 友善結構化資料

    <?php
    function renderPaginationSchema(Paginator $p): string {
        $totalPages = $p->getTotalPages();
        if ($totalPages <= 1) return '';
        
        $schema = [
            '@context' => 'https://schema.org',
            '@type' => 'ItemList',
            'itemListElement' => []
        ];
        
        foreach ($p->getPageRange() as $i => $page) {
            $schema['itemListElement'][] = [
                '@type' => 'ListItem',
                'position' => $i + 1,
                'item' => $p->buildUrl($page)
            ];
        }
        
        return '';
    }
    ?>

    單元測試

    <?php
    // tests/PaginatorTest.php
    class PaginatorTest extends TestCase {
        public function testPageRange(): void {
            $p = new Paginator(['totalItems' => 100, 'perPage' => 10, 'currentPage' => 5, 'visiblePages' => 5]);
            $this->assertEquals([3,4,5,6,7], $p->getPageRange());
            
            // 邊界測試
            $p->currentPage = 1;
            $this->assertEquals([1,2,3,4,5], $p->getPageRange());
            
            $p->currentPage = 10;
            $this->assertEquals([6,7,8,9,10], $p->getPageRange());
        }
    
        public function testBuildUrl(): void {
            $p = new Paginator(['baseUrl' => 'https://example.com/list', 'queryParams' => ['sort' => 'desc']]);
            $this->assertEquals('https://example.com/list?sort=desc&page=3', $p->buildUrl(3));
        }
    }
    ?>

    這個分頁元件解決了實務上常見問題:查詢參數保持、多框架樣式切換、AJAX 整合、SEO 結構化資料、邊界條件處理。建議專案建立統一的 `PaginationService` 封裝常用邏輯,減少控制器重複代碼。

    1. 上一篇文章:
    2. 下一篇文章:
    Copyright © 程式碼之家 All Rights Reserved