PHP分页类怎么封装

wen PHP项目 2

PHP分页类怎么封装?从零构建高性能、可复用的分页组件(附完整代码)


目录导读

  1. 为什么需要封装分页类?——从重复代码到优雅复用
  2. 分页类的核心设计原则——单一职责与可扩展性
  3. PHP分页类封装实战——构造函数、核心方法、HTML渲染
  4. 高级特性:AJAX分页与样式定制——让组件更灵活
  5. 常见问题问答(FAQ)——解决你90%的分页困惑
  6. 性能优化与安全注意——防止SQL注入与大数据量优化

为什么需要封装分页类?

在开发新闻列表、商品展示、用户管理等后台功能时,分页几乎是必选项,如果每个页面都复制粘贴LIMIT offset, size的SQL逻辑和一堆杂乱的<ul>分页链接HTML,不仅代码冗余、难以维护,还会导致以下问题:

PHP分页类怎么封装

  • SQL注入风险:手动拼接$_GET['page']未强制转换为整型。
  • 逻辑分散:计算总页数、当前页范围、循环输出页码的代码散落在各个控制器中。
  • UI难以统一:不同开发者写的分页样式五花八门,后期调整成本高。

核心价值:封装一个分页类,你只需传入总记录数每页条数当前页,即可得到安全的偏移量现成的HTML分页导航,这符合DRY(Don't Repeat Yourself)原则,也是现代MVC框架(如Laravel、ThinkPHP)内部组件的设计思路。


分页类的核心设计原则

在动手写代码前,必须明确三个设计目标,否则封装的类容易变成“一次性用品”:

  • 单一职责:类只负责计算分页数据和生成HTML,绝不包含数据库查询逻辑(查询由调用方完成)。
  • 参数过滤:所有从$_GET获取的页码必须通过intval()abs()处理,防止负数、小数和非法字符。
  • 可定制化:提供可选的CSS类名、链接模板(如?page={page}),以适配Bootstrap、Tailwind等不同前端框架。

PHP分页类封装实战:完整代码与解析

以下是一个纯粹、高效、无框架依赖的PHP分页类,它被设计为单文件、零依赖,可直接拖入你的项目中。

<?php
/**
 * Class Pagination
 * 高性能PHP分页类 - 支持AJAX、自定义样式、安全过滤
 */
class Pagination
{
    // 基本属性
    private int $totalItems;       // 总记录数
    private int $perPage;          // 每页条数
    private int $currentPage;      // 当前页(已安全处理)
    private int $totalPages;       // 总页数
    // 配置选项
    private array $options = [
        'url_pattern' => '?page={page}',      // URL模板
        'css_class' => 'pagination',          // 包裹层CSS类
        'first_text' => '«',                  // 第一页按钮文本
        'prev_text' => '‹',                   // 上一页文本
        'next_text' => '›',                   // 下一页文本
        'last_text' => '»',                   // 最后一页文本
        'show_first_last' => true,            // 是否显示首尾按钮
        'range_pages' => 2,                   // 当前页周围显示页码数
        'ajax' => false,                      // 是否启用ajax模式
        'ajax_target' => '',                  // ajax请求的容器ID
    ];
    /**
     * @param int $total 总记录数(必须)
     * @param int $perPage 每页条数(默认10)
     * @param int $currentPage 当前页码(未过滤的$_GET值)
     * @param array $options 配置覆盖选项
     */
    public function __construct(int $total, int $perPage = 10, int $currentPage = 1, array $options = [])
    {
        $this->totalItems = max(0, $total);
        $this->perPage = max(1, $perPage);
        // 计算总页数(核心公式)
        $this->totalPages = (int)ceil($this->totalItems / $this->perPage);
        // 安全处理:强制非负整数,防止超出边界
        $this->currentPage = max(1, min($totalPages, (int)$currentPage));
        // 合并用户自定义配置
        $this->options = array_merge($this->options, $options);
    }
    /**
     * 获取SQL中LIMIT的偏移量 (用于查询)
     * @return int
     */
    public function getOffset(): int
    {
        return ($this->currentPage - 1) * $this->perPage;
    }
    /**
     * 获取LIMIT的条数
     * @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风格示例)
     * @return string
     */
    public function render(): string
    {
        if ($this->totalPages <= 1) {
            return ''; // 只有一页时不显示分页
        }
        $html = sprintf('<nav aria-label="Page navigation"><ul class="%s">', $this->options['css_class']);
        // 辅助:生成带替换的链接
        $getLink = function ($page) {
            $url = str_replace('{page}', $page, $this->options['url_pattern']);
            if ($this->options['ajax']) {
                // 如果是AJAX模式,挂载onclick事件
                return sprintf('href="javascript:void(0);" onclick="%s"', 
                    "loadPage('{$url}', '{$this->options['ajax_target']}', {$page})");
            }
            return "href='{$url}'";
        };
        // 1. 首页和上一页
        if ($this->options['show_first_last']) {
            $html .= sprintf('<li class="page-item%s"><a class="page-link" %s>%s</a></li>',
                ($this->currentPage == 1) ? ' disabled' : '', 
                $this->currentPage > 1 ? $getLink(1) : '', 
                $this->options['first_text']);
        }
        $html .= sprintf('<li class="page-item%s"><a class="page-link" %s>%s</a></li>',
            ($this->currentPage == 1) ? ' disabled' : '',
            $this->currentPage > 1 ? $getLink($this->currentPage - 1) : '',
            $this->options['prev_text']);
        // 2. 动态页码(含省略号)
        $start = max(1, $this->currentPage - $this->options['range_pages']);
        $end = min($this->totalPages, $this->currentPage + $this->options['range_pages']);
        // 处理左侧省略号
        if ($start > 1) {
            $html .= '<li class="page-item"><a class="page-link" ' . $getLink(1) . '>1</a></li>';
            if ($start > 2) $html .= '<li class="page-item disabled"><span class="page-link">…</span></li>';
        }
        // 循环输出中间的页码
        for ($i = $start; $i <= $end; $i++) {
            $activeClass = ($i == $this->currentPage) ? ' active' : '';
            $html .= sprintf('<li class="page-item%s"><a class="page-link" %s>%d</a></li>',
                $activeClass, $getLink($i), $i);
        }
        // 右侧省略号
        if ($end < $this->totalPages) {
            if ($end < $this->totalPages - 1) {
                $html .= '<li class="page-item disabled"><span class="page-link">…</span></li>';
            }
            $html .= '<li class="page-item"><a class="page-link" ' . $getLink($this->totalPages) . '>' . $this->totalPages . '</a></li>';
        }
        // 3. 下一页和末页
        $html .= sprintf('<li class="page-item%s"><a class="page-link" %s>%s</a></li>',
            ($this->currentPage == $this->totalPages) ? ' disabled' : '',
            $this->currentPage < $this->totalPages ? $getLink($this->currentPage + 1) : '',
            $this->options['next_text']);
        if ($this->options['show_first_last']) {
            $html .= sprintf('<li class="page-item%s"><a class="page-link" %s>%s</a></li>',
                ($this->currentPage == $this->totalPages) ? ' disabled' : '',
                $this->currentPage < $this->totalPages ? $getLink($this->totalPages) : '',
                $this->options['last_text']);
        }
        $html .= '</ul></nav>';
        return $html;
    }
}

使用范例(在控制器或业务逻辑中):

// 1. 获取总数量(假设从数据库查询)
$total = 1000; // 示例值
$perPage = 20;
$page = $_GET['page'] ?? 1;
// 2. 实例化分页类
$pager = new Pagination($total, $perPage, $page, [
    'url_pattern' => '/user/list?page={page}',
    'css_class' => 'pagination justify-content-center', // Bootstrap 4/5类
    'ajax' => true,
    'ajax_target' => '#resultList',
]);
// 3. 获取SQL偏移量用于数据库查询
$offset = $pager->getOffset();
$limit = $pager->getLimit();
// 伪SQL:SELECT * FROM users LIMIT $offset, $limit
// 4. 在视图模板中输出HTML
echo $pager->render();

高级特性:AJAX分页与样式定制

AJAX模式是通过在渲染时生成onclick="loadPage(...)"实现的,你需要在前端定义loadPage函数:

function loadPage(url, containerId, page) {
    fetch(url, {
        headers: { 'X-Requested-With': 'XMLHttpRequest' }
    })
    .then(res => res.text())
    .then(data => {
        document.getElementById(containerId).innerHTML = data;
        // 更新浏览器的当前URL(可选)
        history.pushState({}, '', url.split('&')[0] + '&page=' + page);
    })
    .catch(err => console.error(err));
}

样式定制:分页类输出的是标准<ul>结构,使用Bootstrap 5无需额外CSS即可获得美观样式,对于Tailwind CSS,你可以自定义css_class为类似'flex space-x-2',但要调整内部<li>结构的话,需要修改类内部代码,建议封装时提供setHtmlTemplate()方法实现完全自定义,但核心计算逻辑已经足够健壮。


常见问题问答(FAQ)

问1:如果总记录数为0,分页类会怎样? 答:构造函数中totalItems被强制为max(0, $total),此时totalPages为0。render()方法检测到totalPages <= 1时返回空字符串,不会产生任何输出,安全无报错,同时getOffset()返回0,SQL返回空结果集。

问2:如何防止分页SQL注入? 答:本类的getOffset()getLimit()均返回纯整数类型(通过max()和强制类型转换),不直接拼接SQL字符串,即使$_GET['page']传入了"1 OR 1=1",转换为(int)后变为1,彻底杜绝注入可能。

问3:我的URL是SEO友好的伪静态格式(如/list/5.html),怎么适配? 答:只需修改url_pattern'/list/{page}.html'即可,如果页面需要传递多个参数,建议构造模式如'?category=5&page={page}',该类会智能替换{page}占位符。

问4:中间的“…”省略号怎么调整出现阈值? 答:通过range_pages选项控制,默认值为2,即当前页前后各显示2个页码,如果设置为3,显示范围更大,当页码差距过大时,自动用“…”填充,避免导航过长。

问5:我可以让“首页”和“末页”不显示吗? 答:可以,在实例化时传入'show_first_last' => false即可,适合移动端界面或短分页场景。

问6:分页类能否与PDO预处理语句完美配合? 答:完全可以,PDO的bindValue()支持传递整型参数,示例:

$stmt = $pdo->prepare("SELECT * FROM articles LIMIT :offset, :limit");
$stmt->bindValue('offset', $pager->getOffset(), PDO::PARAM_INT);
$stmt->bindValue('limit', $pager->getLimit(), PDO::PARAM_INT);
$stmt->execute();

性能优化与安全注意

  • SQL层面:对于超大偏移量(如LIMIT 100000, 20),数据库性能会急剧下降,建议在业务层加入“游标分页”或“键集分页”(基于上一页最后一条记录的ID),本类的设计支持扩展该优化(但需自行修改业务查询逻辑)。
  • 缓存总记录数:总页数的计算依赖COUNT(*),在大表上非常耗时,建议在业务层使用Redis缓存总记录数,失效时间设为5分钟或随内容更新。
  • XSS防护:所有HTML转义由前端框架处理,本类只输出纯数字和链接,但如果你在URL模板中拼接了用户变量,务必使用htmlspecialchars()
  • 安全的上限:构造函数中$currentPage = min($totalPages, (int)$currentPage)限制了最大页码,防止恶意构造page=999999导致大量SQL查询。

这个分页类用30行左右的核心代码解决了90%的日常需求,它安全、易扩展、无依赖,且可直接嵌入Laravel、CodeIgniter、或原生PHP项目,封装的价值在于“一次编写,处处使用”,结合你项目的实际URL规则,调整url_pattern和CSS类名即可完美融合,Happy Coding!

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