必须用 paginate() 而非 skip()+take(),它自动处理分页参数、保留查询串并返回 LengthAwarePaginator;列表逻辑应抽离至服务类,参数需校验白名单,关联数据必须 with() 预加载。

分页必须用 paginate(),别手写 skip() + take()
手动计算偏移量容易出错,且无法自动携带当前查询参数(比如搜索关键词、筛选状态),paginate() 会自动读取 page 参数、生成元数据、保留原有查询串。它返回的是 LengthAwarePaginator 实例,含 links()、currentPage()、hasPages() 等方法,视图里直接用就行。
常见错误现象:
- 点第二页后搜索条件丢失
- 总条数显示为 0 或不准确
- 上一页/下一页按钮失效或跳转到根路径
正确做法:
- 始终调用
User::where(...)->paginate(15),不要拆成->skip()->take() - 如果需要保留额外查询参数(如
status=active),链式加->withQueryString() - 若路由路径与实际访问路径不一致(如部署在
/admin子目录),加->withPath('/admin/users')
列表逻辑不能塞进控制器,得抽到服务类或查询对象
控制器只该做三件事:接收请求、调用业务逻辑、返回响应。一旦你发现控制器里出现条件判断嵌套、多表关联拼接、状态转换逻辑,说明它已经超载了。
典型坏味道:
-
if ($request->has('export')) { ... }导出分支混在列表方法里 - 对用户角色做不同字段筛选:
if (auth()->user()->isEditor()) { ... } - 手动合并多个模型结果再排序分页
推荐做法:
- 把筛选、排序、导出等行为封装进
UserListQuery类,构造时传$request,调用execute()返回Collection或Paginator - 导出单独走
ExportUsersAction,和列表控制器解耦 - 权限相关判断移到策略(Policy)或中间件,控制器只负责“调用”,不负责“决策”
搜索和筛选参数必须校验,否则分页会崩
未过滤的请求参数直接进 where(),轻则 SQL 报错,重则分页器崩溃并返回 500。Laravel 不会自动忽略空值或非法字段名,paginate() 之前那段查询构建过程就是高危区。
容易踩的坑:
-
->where($request->key, $request->value)——$request->key是用户可控字符串,可能传入password或id导致意外泄露 -
->orderBy($request->sort, $request->order)—— 若未限制白名单,可被注入asc; DROP TABLE users(虽 Laravel 有防注入,但 orderBy 字段不走绑定,仍需校验) - 传入非整数
page值(如page=abc),paginate()内部会报InvalidArgumentException
解决方式:
- 用
$request->validate()显式声明允许的字段和类型,例如:['page' => 'integer|min:1', 'status' => 'in:active,inactive,pending'] - 字段名白名单硬编码:
$allowedSorts = ['name', 'email', 'created_at'];,再检查in_array($request->sort, $allowedSorts) - 所有动态
where条件前加isset()和类型判断,避免空数组或 null 进入查询
with() 预加载不是可选项,是必选项
没加 with() 的列表页,在 N+1 场景下,100 条数据可能触发 101 次查询——1 次主查 + 每条记录 1 次关联查。分页本身不会缓解这个问题,paginate(15) 只限制主查询结果数,但每条结果渲染时仍会各自触发关联查询。
典型症状:
- 页面加载明显变慢,数据库连接数飙升
- 日志里反复出现同一条 SQL(如
select * from posts where user_id = ?) - 使用
DB::enableQueryLog()查看,发现查询数远大于预期
实操要点:
- 只要模板里用了
$user->posts->count()或@foreach ($user->posts as $post),就必须在控制器里加->with('posts') - 多级关联用点号:
->with('posts.comments.author'),但注意深度不宜超过 3 层,否则内存压力大 - 避免在闭包中用
load()补查——那是懒加载,已脱离分页上下文,会导致分页总数不准
最常被忽略的一点:分页对象本身是“半惰性”的。它只在第一次访问 $users->items() 或遍历的时候才真正执行查询。如果你在控制器里做了多次 count()、first() 或 isEmpty() 判断,又没缓存结果,可能触发重复查询。建议尽早调用 ->appends() 或 ->withQueryString(),然后统一传递给视图,别在视图里反复试探分页对象的状态。


















