跳转到主要内容
如果希望在移动或重命名包的模型后,仍能继续读取多态关联中已存储的数据,就要将模型的类名与存入数据库的类型名分离。Morph map 的别名与其说是简写,不如说是跨版本维护的存储格式契约。 本页基于 Laravel Framework 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 检索所有值。

将升级步骤纳入发布

下面是将旧类名转换为别名的代码片段。请将其实现为目标应用的数据迁移,不要从服务提供者中自动执行。
仅靠这一步并不能完成部署。
  1. 调查目标 *_type 列及其存储值,在更新前准备好备份、记录数确认和恢复步骤。如果其他表也引用了同一模型,也要一并纳入。
  2. 首次引入时,要确定包含停止写入或维护在内的切换步骤,避免写入旧类名的进程与写入别名的进程同时存在。对于大表,还要考虑更新锁和分批处理。
  3. 切换映射表和数据,在不改变主键值的前提下转换类型名。迁移代码中的旧类名应固定为过去实际存储的字符串,而不是会因将来重命名而改变含义的 Article::class。
  4. 确认旧值的残留数量、新的保存、双向关联获取以及预加载,在更新长时间运行的 worker 之后再恢复写入。
  5. 回滚时,要确认回退后的代码能否解析已存储的别名。仅回退 Composer 版本,并不能恢复数据库中的类型名。
如果保持已固定的别名,只修改模型的命名空间,那么将映射表的值改为新类即可继续使用原有的类型名。另一方面,修改别名本身应视为伴随数据迁移的变更。升级步骤也要与包的版本兼容性管理和包迁移的发布与更新保持一致。

测试存储值与双向获取

在 Orchestra Testbench 中,不仅要验证映射表,还要验证存入数据库的值和双向获取。以下示例假设通过 Composer 自动加载包,并已准备好 PDO 和 SQLite 驱动。服务提供者在测试中显式注册,与自动发现的验证分开。
映射表和强制标志是相互独立的静态状态,因此不要认为仅重新创建应用就会将其初始化。本例对两者都进行了保存和恢复。在生产环境中也不要按请求或租户改写它们,而应仅限于启动时的稳定注册。已创建的关联会保存类型名,因此中途更改注册也不会统一反映到现有实例上。 还要在包的 CI 中添加以下回归用例。
  • 在使用方应用已启用严格模式的状态下注册服务提供者,不会将其关闭。
  • 在同一别名冲突、同一个类有多个别名、替换了具体模型的情况下,已公开的存储格式保持不变。
  • 准备以升级前类名存储的 fixture,迁移后旧值消失,双向获取与记录数一致。
  • 包含队列 worker 等在内的实际升级步骤,能够通过回退后的代码与数据组合进行恢复。

一手资料

最后修改于 2026年11月11日