afterQuery()。它不仅能添加 SQL 条件,还能向查询构建器注册一个对查询结果执行的 Closure。
不过,它并不是”每执行一次 SQL 就会收到相同类型结果的钩子”。根据查询方法的不同,传入的 Collection 内容和调用次数都会发生变化。
本页面面向 Laravel 13。以官方文档中的 After Query Callbacks为起点,结合 Eloquent 构建器和官方测试确认使用时的边界。
将结果处理集中到本地作用域
如果需要反复通过 ID 引用模型,可以准备一个统一结果键的作用域。以下示例定义在app/Models/Post.php 中。
$posts 仍然是 Eloquent Collection,元素也仍然是 Post。get(42) 返回键为 42 的模型,不存在时返回 null。如果使用 select() 限定查询列,请同时包含用作键的 id。
如果只在一个地方处理,get()->keyBy('id') 更简单。当你希望在作用域或复用的查询中也包含查询后的处理时,再选择 afterQuery()。作用域本身的定义方法请参阅 Eloquent 作用域。
解读 get 的执行顺序
Eloquent\Builder::get() 会应用全局作用域、生成模型、对指定的关联进行预加载,最后执行 afterQuery 的回调。
在普通的 get() 中,回调可以使用已预加载的关联。访问未指定的关联可能会产生额外的 SQL,因此请结合预加载进行设计。
它与 retrieved 模型事件的时机不同。retrieved 在 Model::newFromBuilder() 中生成每个模型时触发,早于 get() 的预加载。处理一批查询结果模型的 afterQuery() 与每个模型的事件并不承担相同的职责。
返回值会传递给下一个回调
afterQuery() 按注册顺序添加 Closure。负责执行的 applyAfterQueryCallbacks() 实现如下。
??,而是基于 PHP truthy 判断的 ?:。
也可以使用直接修改 Collection 而不返回任何内容的
void 回调。此时会保留被修改的同一个对象。另一方面,如果创建了新的 Collection,就必须返回该 Collection。
内部的参数和返回值都是 mixed,但应避免把任意类型转换作为公开 API。例如,将 get() 的结果替换为字符串或数组,会破坏 first() 随后调用的 ->first() 等处理。在作用域中,应以保持模型、Collection 类型、数量和排序的处理为基本原则。
各查询方法的差异
下表是注册在 Eloquent 构建器上的afterQuery() 的处理方式。
first() 在内部调用 limit(1)->get()->first()。不能因为使用了 first() 就把 Closure 的参数改成 Post。
如果只需要值,不要使用作用域,直接写 Post::query()->pluck('id')。如果要在处理模型之后再取出值,可以写 Post::keyedById()->get()->pluck('id')。后者的 pluck() 是对已查询 Collection 的操作,而不是对查询构建器的操作。
不要改变每页数量或下一次查询位置
paginate() 的总数通过 SQL 统计,之后再查询当前页的模型。即使在查询后过滤 Collection,总数也不会重新计算。
另外,simplePaginate() 和 cursorPaginate() 会使用处理后的结果判断是否有下一页。chunkById() 使用分块末尾的 ID 作为下一次查询的位置。如果在回调中改变数量或顺序,分页判断和继续位置可能会与原本的查询不一致。
筛选、排序和查询列应在 SQL 一侧指定,afterQuery() 只用于不改变查询结果含义的处理。如果只是想改变 API 的展示格式,也可以考虑使用 API 资源进行转换。
官方文档中有一个向模型添加搜索结果摘要属性的示例。对模型属性的赋值也可能改变要保存的内容。请避免将带有数据库中不存在的展示用属性的模型直接 save() 的设计。
注册会保留在查询构建器上
每次调用afterQuery() 都会添加一个 Closure,执行后注册也不会消失。使用同一个构建器再次查询时会再次执行,同一个作用域调用两次,回调也会被注册两次。克隆构建器时,也会继承已注册的回调。
在需要多次查询的处理或长生命周期的 worker 中,应为每个用途创建新的查询,并让回调只做无副作用的结果处理。如果在其中放入发送邮件或持久化操作,很容易把分块或游标带来的多次执行误认为业务上的一次处理。
在扩展包中提供可复用的作用域时,也请将支持的查询方法作为公开 API 明确说明,并测试类型和调用次数的契约。
测试不同查询路径的差异
在使用方的 Laravel 13 应用中,不仅要确认键的处理,还要确认first()、cursor() 和空页时的调用情况。
以下示例假设已有前面的 Post 模型,以及包含 id、title、body、created_at、updated_at 的 posts 表迁移。使用 RefreshDatabase 隔离测试之间的记录。
在 tests/Feature/AfterQueryTest.php 中创建测试。
维护时的检查清单
- 明确作用域支持的查询方法,以及 Closure 的参数和返回值类型。
- 区分验证
get()的空结果与总数为 0 的paginate()。 - 对于分块和游标,确认调用次数和查询顺序不会改变。
- 设计时确保回调重复注册或构建器复用不会产生业务上的副作用。
- 更新 Laravel 时,重新确认
get()、pluck()、cursor()以及BuildsQueries的调用路径。
相关页面
Eloquent 作用域
了解将 SQL 约束集中到本地作用域和全局作用域的基础知识。
Eloquent 集合
了解处理模型的 Collection 与 Support Collection 的区别。
Eloquent Observers 与模型事件
了解与查询结果处理不同的、以模型为单位的事件。
API 资源
将返回给 API 的展示数据转换与模型的保存处理分离。