PHP分页工具类从入门到精通:构建高性能、可复用的分页组件(附完整代码)
目录导读
- 为什么你需要一个分页工具类? —— 告别重复劳动,提升开发效率
- 分页核心原理解析 —— 从SQL的LIMIT到数据总量的计算
- 设计一个健壮的分页工具类 —— 面向接口、可扩展、防御式编程
- 实战:完整的分页工具类代码 —— 带注释,开箱即用
- 如何集成到你的项目? —— 与MySQLi、PDO、Laravel无缝对接
- 高级优化与陷阱规避 —— 大数据量下的性能飞升与常见错误
- 常见问题问答(FAQ) —— 解决你最后的疑惑
为什么你需要一个分页工具类?
在开发任何涉及列表展示的Web应用时(如博客文章、商品列表、用户管理),分页几乎是无法回避的刚性需求,如果每个页面都从零开始写分页逻辑,你会陷入大量的重复代码:计算偏移量、拼接SQL、生成HTML链接、处理边界条件……

痛点:
- 代码冗余,难以维护。
- 分页链接极易出错(例如当前页高亮、首页/尾页显示)。
- 没有统一的总页数计算逻辑,容易导致数组越界或查询错误。
解决方案:一个封装良好的 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,否则会查询出空数据。
设计一个健壮的分页工具类
一个优秀的分页类应遵循以下设计原则:
- 构造函数注入配置:通过构造函数传入
$totalItems与$perPage,避免内部硬编码。 - 链式调用(可选):使用
setCurrentPage()等方法返回$this,方便配置。 - 输出分离:提供
getHtml()返回渲染后的HTML字符串,而不是直接echo,这样便于在模板中使用。 - 防御式编程:对传入的
$currentPage做intval处理,如果小于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>结构。