resolveRelationUsing() 注册关联的创建方式。无需直接改写模型文件,就能像普通关联一样进行获取、预加载以及按存在条件查询。
本页面基于对 Laravel Framework v13.40.0 官方源码的确认。关联的基础请参阅Eloquent 关联入门,服务提供者的加载请参阅包的自动发现。
与普通定义区分使用
官方文档建议在一般的应用开发中使用普通的关联方法,并指出动态定义在包开发等场景中可能会有用。
动态关联属性与从模型外部注册关联定义的功能是两回事。此外,
resolveRelationUsing() 并不是用于添加任意方法的 Macro。回调应设计为返回 Eloquent 的 Relation 实例。
注册到使用者选择的模型
作为示例,我们为使用方应用的订单模型添加一个名为acmeNotes 的 hasMany。假设包的命名空间 Acme\OrderNotes\ 通过 Composer 的 PSR-4 对应到 src/,并作为普通的非延迟服务提供者加载。
准备配置与关联目标模型
config/acme-order-notes.php 的初始值设为 null。仅安装包并不会为应用的模型注册关联。
src/Models/Note.php 是关联目标模型。
id,acme_order_notes 表有 id、order_id、body 和时间戳。请为 order_id 创建索引。如果使用 UUID 等,也要让关联列的类型保持一致。
动态关联的注册不会创建表或外键约束。这与包迁移的发布与更新是不同的处理。下面的服务提供者中也不包含数据库访问或迁移执行。
在 boot 中注册创建方式
在src/OrderNotesServiceProvider.php 中合并配置,并在 boot() 中为目标模型注册回调。
order_model 修改为实际使用的具体模型,例如 \App\Models\Order::class。服务提供者的加载通过自动发现进行配置。
hasMany() 中显式指定了外键 order_id 和父模型一侧的键。这样即使父模型的类名是 Purchase,也不会推断为 purchase_id,从而与包的表定义保持一致。
像普通关联一样使用
对于配置的订单模型,方法调用返回HasMany,属性访问返回获取结果的集合。
create() 中,hasMany 会设置 order_id,因此无需在使用者传入的数组中混入外键。反向关联不会自动添加,如有需要请另行定义。
不要在回调中执行到获取数据
如果回调的返回值是get() 的结果或单个模型,那么在属性访问等时候就无法作为有效的关联处理。请将关联的构建与数据获取分离。
预加载时的 Builder::getRelation() 会使用新的模型实例来构建关联。因此,应避免让回调以”已存在获取到的订单 ID”为前提,或根据该 ID 返回不同的关联定义。
即使是动态注册,N+1 问题和延迟加载(Lazy Loading)的限制也不会消失。处理多个模型时,请照常使用 with() 等加载关联。
使用 belongsTo 时也要注意关联名的推断
belongsTo() 在省略关联名时,会通过回溯(backtrace)推断调用方。在闭包中定义时,不要依赖与普通模型方法相同的推断,除了外键和所有者键之外,还可以通过第 4 个参数显式指定关联名。
下面是订单一侧有 customer_id、客户一侧有 id 时的独立示例。
理解名称冲突与继承的规则
HasRelationships 以模型类名与关联名为键,将闭包保存到 static 属性中。接收到不存在方法调用的 Model::__call() 会查找 resolver,并传入目标模型实例来执行。
图中展示的是找到 resolver 时的路径。请注意,与已有方法或属性冲突时会走不同的路径。
关联名请使用像
acmeNotes 这样独特的名称,并确认不会与使用者的方法、属性以及其他包冲突。仅凭 method_exists() 无法确认动态注册是否存在。Eloquent 的 isRelation() 也会检查 resolver,但并不能保证不与属性冲突。
区分注册状态与获取结果的生命周期
resolver 的注册与各模型持有的已加载关联数据是两回事。
即使在已读取
$order->acmeNotes 之后覆盖 resolver,该模型持有的结果也不会自动更新。如需重新获取,请先移除关联结果,再使用 load('acmeNotes') 等。
同样,不要认为仅在 Testbench 中重新创建应用就能让 resolver 消失。比较启用与禁用状态的测试,请使用不同的模型 fixture,或使用 PHPUnit 的进程隔离,避免依赖之前的注册。
同时测试注册与 SQL
在 Orchestra Testbench 中,不仅要验证服务提供者的注册,还要验证外键和实际的关联获取。下面是tests/DynamicRelationsTest.php 的示例。请确保包的类和测试的命名空间可以通过 Composer 加载,并准备好 PDO 和 SQLite 驱动。
OrderFixture,但外键仍是 order_id 而不是 order_fixture_id。由此可以确认没有依赖于根据类名进行的推断。
在包的 CI 中,还应验证以下边界情况。
- 对于未注册的其他 fixture,当
order_model为null时不添加关联。 - 向子模型的继承,以及仅在子模型中注册同名 resolver 时的优先顺序。
- 与已有方法或属性冲突时,预期的 API 仍然可用。
- 已加载过关联的模型与新模型在 resolver 重新注册后的差异。
- 所支持数据库中的列类型、索引、外键约束,以及使用者修改过的主键。