v13.42.0 的官方实现。关联的基础请参阅 Eloquent 关联入门,从外部为使用方模型添加关联的方法请参阅包的动态关联注册。
区分类型名与关联名
多态关联通过组合关联目标的主键与类型名来引用多种模型。例如,在评论的关联目标可以是文章或视频的结构中,不仅需要commentable_id,还需要 commentable_type。
未注册时,通常会使用
Acme\Publishing\Models\Article 这样的完全限定类名作为类型。注册别名后,在创建新关联等场景中会使用 acme-publishing.article。仅靠注册并不会改写现有行中的值。
Morph map 并不会创建关联方法或数据表。模型、列和索引照常准备即可。
只注册包专属的别名
示例中的Acme\Publishing\ 通过 Composer 的 PSR-4 对应到 src/。首先定义 src/Models/Article.php。
src/Models/Comment.php 中定义反向关联。
acme_articles 包含 id、title 和时间戳,acme_comments 包含 id、body、commentable_type、commentable_id 和时间戳。本例的主键为整数。可以使用 $table->morphs('commentable') 创建的列和复合索引,但在使用 UUID 等的结构中,请让列类型与关联目标保持一致。
接着,在普通的非延迟服务提供者 src/PublishingServiceProvider.php 的 boot() 中注册别名。
$alias 为 acme-publishing.article,$class 为 Article::class。只要 Comment 本身不会成为其他多态关联的关联目标,就无需为本例注册 Comment 的别名。
理解合并与名称冲突的规则
Relation::morphMap() 操作的是静态映射表。默认合并时,会用 $map + static::$morphMap 将新数组与现有数组合并。
如果包在第二个参数中传入
false,就会清除其他包和使用方应用的注册。常规注册时应保留现有映射表,并选择能识别发布方和包的别名,而不是 article 这样的通用名称。不要将同一个别名复用为不同的含义。
只传入类名列表时,Laravel 会实例化每个模型并以表名作为键。如果包希望存储格式也不受表名变更影响,请像上面的示例那样显式声明别名与类的对应关系。
标准的 getMorphClass() 查找的是具体模型的类。即使使用 Article 的子类,也不会自动继承父类的别名。支持替换模型的包,需要设计成将实际使用的类注册到映射表中。
将严格模式作为整个应用的方针
官方文档中的enforceMorphMap() 会一并完成别名注册和 requireMorphMap(true)。这并不是仅限于包的强制。
在
v13.42.0 的标准实现中,严格模式会影响以下两个方向。
- 从模型到类型名:
getMorphClass()会对未注册的模型抛出ClassMorphViolationException。内置的通用Pivot类有例外处理,但普通模型和自定义 Pivot 没有同样的豁免。 - 从已存储的类型名到类:
getActualClassNameForMorph()会对不在映射表键中的值抛出同样的异常。morphTo()的常规获取和预加载也会用到它。即使类出现在映射表的“值”中,只要旧的完全限定类名不在“键”中,就无法加载。
morphMap() 注册自己的模型,是否在整个应用中强制由使用方决定。如果使用方已经启用了强制,不要在包中将其改回 requireMorphMap(false)。
先迁移现有数据再判断兼容性
引入别名注册时,新安装与现有应用升级的条件不同。官方文档也提醒,引入时需要将现有的完全限定类名转换为别名。只确认反向获取是不够的
严格模式关闭时,只要旧commentable_type 中的类仍然存在,有时仍能从评论通过 morphTo() 获取文章。但文章一侧的 morphMany() 会按 getMorphClass() 的值筛选类型。注册别名后它会查找 acme-publishing.article,因此使用旧类名的行会从文章的评论列表中消失。
MorphOneOrMany 在创建时保存类型名,并将其同时用于查询条件和新建记录。即使新旧类型名混杂,它也不会自动以 OR 检索所有值。
将升级步骤纳入发布
下面是将旧类名转换为别名的代码片段。请将其实现为目标应用的数据迁移,不要从服务提供者中自动执行。- 调查目标
*_type列及其存储值,在更新前准备好备份、记录数确认和恢复步骤。如果其他表也引用了同一模型,也要一并纳入。 - 首次引入时,要确定包含停止写入或维护在内的切换步骤,避免写入旧类名的进程与写入别名的进程同时存在。对于大表,还要考虑更新锁和分批处理。
- 切换映射表和数据,在不改变主键值的前提下转换类型名。迁移代码中的旧类名应固定为过去实际存储的字符串,而不是会因将来重命名而改变含义的
Article::class。 - 确认旧值的残留数量、新的保存、双向关联获取以及预加载,在更新长时间运行的 worker 之后再恢复写入。
- 回滚时,要确认回退后的代码能否解析已存储的别名。仅回退 Composer 版本,并不能恢复数据库中的类型名。
测试存储值与双向获取
在 Orchestra Testbench 中,不仅要验证映射表,还要验证存入数据库的值和双向获取。以下示例假设通过 Composer 自动加载包,并已准备好 PDO 和 SQLite 驱动。服务提供者在测试中显式注册,与自动发现的验证分开。- 在使用方应用已启用严格模式的状态下注册服务提供者,不会将其关闭。
- 在同一别名冲突、同一个类有多个别名、替换了具体模型的情况下,已公开的存储格式保持不变。
- 准备以升级前类名存储的 fixture,迁移后旧值消失,双向获取与记录数一致。
- 包含队列 worker 等在内的实际升级步骤,能够通过回退后的代码与数据组合进行恢复。