该做,分页中间件应统一校验页码与限制范围、防止OFFSET过大、注入合法分页参数,但不执行查询或算总数,仅专注边界控制与安全兜底。

分页中间件该不该做?
PHP 分页本身不需要中间件——$_GET['page'] 解析、总数计算、SQL LIMIT 拼接,三步就能跑通。但当你在多个控制器里重复写 $page = max(1, (int)$_GET['page'] ?? 1)、$limit = 20、$offset = ($page - 1) * $limit,还顺手漏校验 $page 是否溢出(比如请求第 10000 页),就该考虑抽中间件了。
中间件的价值不是“炫技”,而是统一边界:防止非法页码穿透到查询层、避免偏移量爆炸(OFFSET 超大时 MySQL 性能断崖)、让控制器只专注业务数据组装。
怎么写一个轻量分页中间件(Laravel 风格)
不依赖框架也能实现,核心就是拦截请求、注入分页参数、挂载到请求对象或全局变量。以 Laravel 的 Middleware 写法为例:
class PaginationMiddleware
{
public function handle($request, Closure $next)
{
$page = max(1, (int)($request->query('page') ?? 1));
$limit = min(100, (int)($request->query('limit') ?? 15)); // 硬限制上限,防滥用
<pre class="brush:php;toolbar:false;"> // 防止 OFFSET 过大:用游标式替代(后续可扩展),此处先做基础兜底
if ($page > 1000) {
throw new \InvalidArgumentException('Page too large');
}
$request->page = $page;
$request->limit = $limit;
$request->offset = ($page - 1) * $limit;
return $next($request);
}}
立即学习“PHP免费学习笔记(深入)”;
- 必须校验
$page和$limit类型与范围,否则整型溢出或 SQL 注入风险仍在 - 不要在中间件里查总数或执行分页查询——那是 Repository 或 Service 层的事
- 若用原生 PHP,可用
$_GET替代$request->query(),但需手动封装Request对象或改用函数式入口
中间件和数据库查询怎么配合才不翻车
分页中间件只管“页码合法性和偏移量生成”,不碰 SQL。真正执行时,必须严格使用 limit 和 offset 参数,而非拼接字符串:
$stmt = $pdo->prepare("SELECT * FROM posts WHERE status = ? ORDER BY id DESC LIMIT ? OFFSET ?");
$stmt->execute([$status, $request->limit, $request->offset]);
$items = $stmt->fetchAll();- 禁止用
"LIMIT {$request->limit} OFFSET {$request->offset}"字符串拼接——哪怕参数已转整型,也存在被绕过的可能 - 总数查询必须独立执行(
SELECT COUNT(*)),且条件要和主查询完全一致,否则会出现“最后一页有数据但总数显示为 0” - 如果用 Laravel Eloquent,中间件后直接调用
->forPage($request->page, $request->limit)即可,它内部已处理好OFFSET安全性
容易被忽略的边界:游标分页要不要现在加?
当数据频繁增删、用户滑动加载时,传统 OFFSET 分页会跳过或重复条目。这不是中间件能自动解决的,但得在设计之初留钩子:
- 中间件可识别
cursor参数(如?cursor=12345),并设置$request->cursor = (int)$_GET['cursor'] ?? null - 控制器判断
$request->cursor !== null时,走基于主键/时间戳的条件查询(WHERE id ),跳过 <code>OFFSET - 别在同一个中间件里混用两种模式——逻辑分支太多,后期维护成本陡增;建议新接口用游标,老接口维持
page,中间件保持参数解析职责单一
真正的难点不在代码,而在分页语义是否和前端对齐:比如“下一页”按钮该传 page=2 还是上一页最后一条的 id。这个得前后端提前约定,中间件只是执行者,不是决策者。











