优先选class,因分页有明确状态(当前页、总条数等)和行为(生成HTML、计算偏移量等);Trait仅适合无状态函数复用,如formatPageUrl()。

翻页逻辑该封装成类还是Trait?
优先选 class,不是因为“更高级”,而是分页本身有明确的状态(当前页、总条数、每页数、URL参数等)和行为(生成HTML、计算偏移量、判断是否有下一页),用类天然适合封装状态+方法。Trait 适合无状态的通用函数复用,比如 formatPageUrl() 这种纯计算逻辑可抽,但整个翻页器硬塞进 Trait 容易让使用者自己拼状态,反而难维护。
核心属性和构造参数怎么设计才不踩坑?
常见错误是把 $currentPage 默认设为 1 并忽略请求来源——结果用户点第5页,URL没变,$_GET['page'] 是空,类却默默回退到第1页,体验断裂。正确做法是显式接收并校验:
-
$totalItems必须传,且应为int,负数或非数字直接抛异常,避免后续计算出错 -
$perPage建议设默认值(如20),但必须检查是否 > 0,否则ceil($totalItems / $perPage)会报除零警告 -
$currentPage不设默认值,强制调用方传入(哪怕传$_GET['page'] ?? 1),并在构造里做max(1, (int)$currentPage)校验,防止传入负数或字符串如"abc" - URL 参数建议用
$urlParams = []接收,而不是硬编码$_GET,方便 CLI 或 API 场景复用
生成分页HTML时最容易漏掉的三件事
很多人只顾拼 <a href="?page=2">2</a>,结果上线后发现:中文URL乱码、带搜索条件的页面翻页丢参数、移动端小屏上一堆数字挤成一团。实际要补:
- URL 构建必须用
http_build_query()合并原有参数,例如:http_build_query(array_merge($this->urlParams, ['page' => $i])),别手拼"?".http_build_query(...),否则可能覆盖已有? - 所有输出的链接 href 和文字内容,都要过
htmlspecialchars(),特别是当$urlParams来自用户输入时 - 加
aria-current="page"到当前页链接,对屏幕阅读器友好;用rel="prev"/rel="next"给搜索引擎提示,不是锦上添花,是基础可访问性
性能敏感场景下,getOffset() 和 getLimit() 要怎么写?
这两个方法常被直接用于 SQL 的 LIMIT 子句,但有人写成 return ($this->currentPage - 1) * $this->perPage;,看着没问题,一到大偏移量(比如第10万页)就拖垮数据库。真正关键的是:这个类不该决定要不要用游标分页,但它得提供清晰接口让上层判断:
-
getOffset()保持简单,只返回数值,不做 DB 层优化 - 额外加一个
shouldUseCursorPagination(): bool方法,内部判断$this->currentPage * $this->perPage > 10000就返回true,提醒调用方该换方案了 - 如果业务确定不用游标,至少在文档里注明:此分页器仅适用于总数据量
< 10w的场景,超量需配合缓存或改查总数逻辑
分页不是越通用越好,关键是让使用者一眼看清边界在哪——比如 $totalItems 是真实 count 还是估算值,这个信息必须由构造时传入的参数名或注释说清,不能藏在方法里猜。
立即学习“PHP免费学习笔记(深入)”;



















