PHP 分页工具类

wen PHP项目 3

PHP分页工具类从入门到精通:构建高性能、可复用的分页组件(附完整代码)


目录导读

  1. 为什么你需要一个分页工具类? —— 告别重复劳动,提升开发效率
  2. 分页核心原理解析 —— 从SQL的LIMIT到数据总量的计算
  3. 设计一个健壮的分页工具类 —— 面向接口、可扩展、防御式编程
  4. 实战:完整的分页工具类代码 —— 带注释,开箱即用
  5. 如何集成到你的项目? —— 与MySQLi、PDO、Laravel无缝对接
  6. 高级优化与陷阱规避 —— 大数据量下的性能飞升与常见错误
  7. 常见问题问答(FAQ) —— 解决你最后的疑惑

为什么你需要一个分页工具类?

在开发任何涉及列表展示的Web应用时(如博客文章、商品列表、用户管理),分页几乎是无法回避的刚性需求,如果每个页面都从零开始写分页逻辑,你会陷入大量的重复代码:计算偏移量、拼接SQL、生成HTML链接、处理边界条件……

PHP 分页工具类

痛点

  • 代码冗余,难以维护。
  • 分页链接极易出错(例如当前页高亮、首页/尾页显示)。
  • 没有统一的总页数计算逻辑,容易导致数组越界或查询错误。

解决方案:一个封装良好的 PHP分页工具类,它能为你提供:

  • 统一逻辑:一次编写,随处调用。
  • 高可配置性:通过参数控制每页条数、显示页码数量、样式。
  • 健壮性:自动处理超出范围页码,防止SQL报错。

分页核心原理解析

在编写类之前,我们必须清楚分页背后的数学与SQL逻辑。

  • 核心变量
    • $totalItems(总记录数): 来自 SELECT COUNT(*) FROM table.
    • $perPage(每页条数): 由开发者设定,如10、20。
    • $currentPage(当前页码): 通过 $_GET['page'] 获取,通常需过滤非法值。
    • $totalPages(总页数): 计算公式为 ceil($totalItems / $perPage)
    • $offset(偏移量): 计算公式为 ($currentPage - 1) * $perPage,SQL语句为:SELECT * FROM table LIMIT $offset, $perPage;

关键点:当$currentPage大于$totalPages时,必须将页码重置为$totalPages,否则会查询出空数据。


设计一个健壮的分页工具类

一个优秀的分页类应遵循以下设计原则:

  1. 构造函数注入配置:通过构造函数传入$totalItems$perPage,避免内部硬编码。
  2. 链式调用(可选):使用setCurrentPage()等方法返回$this,方便配置。
  3. 输出分离:提供getHtml()返回渲染后的HTML字符串,而不是直接echo,这样便于在模板中使用。
  4. 防御式编程:对传入的$currentPageintval处理,如果小于1则强制为1。

实战:完整的分页工具类代码

这是一个经过精简但功能完备的类,包含注释,可直接复制使用。

<?php
/**
 * Class Paginator
 * 一个轻量级、无依赖的PHP分页工具类
 */
class Paginator
{
    private int $totalItems;
    private int $perPage;
    private int $currentPage;
    private int $totalPages;
    private int $offset;
    private string $baseUrl; // 分页链接的基础地址,'?page='
    /**
     * @param int $totalItems 总数据条数
     * @param int $perPage 每页显示条数
     * @param string $baseUrl 基础URL,默认 '?page='
     */
    public function __construct(int $totalItems, int $perPage = 10, string $baseUrl = '?page=')
    {
        $this->totalItems = max(0, $totalItems); // 防止负数
        $this->perPage = max(1, $perPage);       // 防止除零错误
        $this->baseUrl = $baseUrl;
        // 计算总页数
        $this->totalPages = (int)ceil($this->totalItems / $this->perPage);
        // 防止总页数为0(当无数据时)
        $this->totalPages = $this->totalPages > 0 ? $this->totalPages : 1;
        // 设置当前页码(通过GET参数传入,并做安全过滤)
        $page = $_GET['page'] ?? 1;
        $this->currentPage = max(1, (int)$page);
        // 如果当前页超出总页数,强制回落到最后一页
        if ($this->currentPage > $this->totalPages) {
            $this->currentPage = $this->totalPages;
        }
        // 计算偏移量
        $this->offset = ($this->currentPage - 1) * $this->perPage;
    }
    /**
     * 获取当前偏移量(用于SQL LIMIT)
     * @return int
     */
    public function getOffset(): int
    {
        return $this->offset;
    }
    /**
     * 获取每页条数
     * @return int
     */
    public function getLimit(): int
    {
        return $this->perPage;
    }
    /**
     * 获取当前页码
     * @return int
     */
    public function getCurrentPage(): int
    {
        return $this->currentPage;
    }
    /**
     * 获取总页数
     * @return int
     */
    public function getTotalPages(): int
    {
        return $this->totalPages;
    }
    /**
     * 生成带样式的前后端分页HTML(基于Bootstrap 5类,无需额外CSS)
     * @return string 渲染后的HTML
     */
    public function getHtml(): string
    {
        if ($this->totalPages <= 1) {
            return ''; // 只有一页,无需显示分页
        }
        $html = '<nav aria-label="Page navigation"><ul class="pagination justify-content-center">';
        // 上一页按钮
        $prevDisabled = $this->currentPage == 1 ? ' disabled' : '';
        $html .= '<li class="page-item' . $prevDisabled . '"><a class="page-link" href="' . $this->baseUrl . ($this->currentPage - 1) . '">«</a></li>';
        // 页码显示逻辑(显示当前页前后各2页)
        $start = max(1, $this->currentPage - 2);
        $end = min($this->totalPages, $this->currentPage + 2);
        for ($i = $start; $i <= $end; $i++) {
            $active = $i == $this->currentPage ? ' active' : '';
            $html .= '<li class="page-item' . $active . '"><a class="page-link" href="' . $this->baseUrl . $i . '">' . $i . '</a></li>';
        }
        // 下一页按钮
        $nextDisabled = $this->currentPage == $this->totalPages ? ' disabled' : '';
        $html .= '<li class="page-item' . $nextDisabled . '"><a class="page-link" href="' . $this->baseUrl . ($this->currentPage + 1) . '">»</a></li>';
        $html .= '</ul></nav>';
        return $html;
    }
}
// 使用示例(假设控制器中已有$totalItems变量):
// $paginator = new Paginator($totalItems, 10, '?page=');
// $sql = "SELECT * FROM articles LIMIT {$paginator->getOffset()}, {$paginator->getLimit()}";
// echo $paginator->getHtml();

如何集成到你的项目?

  • 原生PHP + MySQLi

    $result = $mysqli->query("SELECT COUNT(*) as cnt FROM articles");
    $row = $result->fetch_assoc();
    $totalItems = $row['cnt'];
    $paginator = new Paginator($totalItems, 10, './list.php?page=');
    $offset = $paginator->getOffset();
    $limit = $paginator->getLimit();
    $dataResult = $mysqli->query("SELECT * FROM articles ORDER BY id DESC LIMIT $offset, $limit");
  • Laravel(核心思想一致): Laravel自带paginate(),如果你出于学习或定制需求想用此类,可以直接在控制器中实例化,将数据传给视图,但推荐使用框架原生方法,除非你需要高度定制。


高级优化与陷阱规避

性能陷阱:当$totalItems有上百万时,每次查询COUNT(*)会耗费资源,建议使用缓存(如Redis)将总数缓存几分钟。

安全陷阱:禁止直接将$_GET['page']拼接到SQL,虽然本类已经做了intval处理,但在其他场景,永远使用预处理语句。

SEO优化:对于Google和Bing,分页链接最好采用rel="prev"rel="next"标记,以加强页面关联性,本类输出的基础HTML虽然没包含,但你可以在外层包装时加上。

用户体验:页码数量固定为5个(前后各2),避免了页码过多难点击的问题,对于大型列表,建议增加“跳转到”输入框。


常见问题问答(FAQ)

问:如果数据库没有任何数据,分页类会不会报错? 答:不会,本类在构造时已将totalPages至少设为1,且currentPage最多为1,getHtml()会返回空字符串,避免显示无用的分页条。

问:我想每页显示50条,怎么修改? 答:只需在实例化时传入第二个参数:new Paginator($totalItems, 50)

问:如何去掉URL中的?page=,改用路径重写如/list/2 答:稍作修改,将baseUrl参数传入/list/,并确保服务器正确解析路由即可。

问:这个类的HTML样式能改吗? 答:完全可以。getHtml()方法返回的是标准的pagination类(Bootstrap采用),如果你用Tailwind或自定义CSS,只需修改类内部的字符串拼接逻辑,或改返回<ul>结构。

抱歉,评论功能暂时关闭!