跳轉到主要內容
若希望在移動或重新命名套件的 model 之後,仍能持續讀取多型關聯中已儲存的資料,就要將 model 的類別名稱與存入資料庫的型別名稱分離。Morph map 的別名與其說是簡寫,不如說是跨版本維護的儲存格式契約。 本頁依據 Laravel Framework v13.42.0 的官方實作。關聯的基礎請參閱 Eloquent 關聯入門,從外部為使用端 model 新增關聯的方法請參閱套件的動態關聯註冊。

區分型別名稱與關聯名稱

多型關聯透過組合關聯目標的主鍵與型別名稱來參照多種 model。例如,在評論的關聯目標可以是文章或影片的架構中,不只需要 commentable_id,也需要 commentable_type。 未註冊時,通常會使用 Acme\Publishing\Models\Article 這類完整限定類別名稱作為型別。註冊別名後,在建立新關聯等情境中會使用 acme-publishing.article。僅靠註冊並不會改寫既有資料列的值。 Morph map 並不會建立關聯方法或資料表。model、欄位與索引照常準備即可。

只註冊套件專屬的別名

範例中的 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 等的架構中,請讓欄位型別與關聯目標一致。 接著,在一般的非延遲 service provider src/PublishingServiceProvider.php 的 boot() 中註冊別名。
Provider 的載入透過套件自動偵測設定。註冊時不需要儲存 model 或更新資料庫中的型別名稱。既有資料的遷移是另一項工作。
$alias 為 acme-publishing.article,$class 為 Article::class。只要 Comment 本身不會成為其他多型關聯的關聯目標,就不需要為本例註冊 Comment 的別名。

理解合併與名稱衝突的規則

Relation::morphMap() 操作的是靜態對應表。預設合併時,會以 $map + static::$morphMap 將新陣列與既有陣列結合。 若套件在第二個引數傳入 false,就會清除其他套件與使用端應用程式的註冊。一般註冊時應保留既有對應表,並選擇能識別發布者與套件的別名,而不是 article 這類通用名稱。不要將同一個別名重複用於不同的意義。 只傳入類別名稱清單時,Laravel 會建立每個 model 的實例並以資料表名稱作為鍵。若套件希望儲存格式也不受資料表名稱變更影響,請像上方範例一樣明確宣告別名與類別的對應。
若使用端應用程式為同一類別新增其他別名,註冊順序可能改變之後儲存的值與關聯查詢的條件。「兩個名稱都註冊了,所以相容性得以維持」並不一定成立。即使允許替換類別,也要將「已儲存的資料能以同一別名讀取」作為公開契約加以確認。
標準的 getMorphClass() 會尋找具體 model 的類別。即使使用 Article 的子類別,也不會自動繼承父類別的別名。支援替換 model 的套件,需要設計成將實際使用的類別註冊到對應表中。

將嚴格模式作為整個應用程式的方針

官方文件中的 enforceMorphMap() 會一併完成別名註冊與 requireMorphMap(true)。這並不是僅限於套件的強制。 在 v13.42.0 的標準實作中,嚴格模式會影響以下兩個方向。
  • 從 model 到型別名稱:getMorphClass() 會對未註冊的 model 拋出 ClassMorphViolationException。內建的通用 Pivot 類別有例外處理,但一般 model 與自訂 Pivot 沒有同樣的豁免。
  • 從已儲存的型別名稱到類別:getActualClassNameForMorph() 會對不在對應表鍵中的值拋出同樣的例外。morphTo() 的一般取得與 Eager 載入也會用到它。即使類別出現在對應表的「值」中,只要舊的完整限定類別名稱不在「鍵」中,就無法載入。
嚴格模式停用時,不在對應表中的型別名稱會被當作類別名稱處理。但這並不保證該類別存在,也不保證它是預期的關聯目標。 套件通常只以 morphMap() 註冊自己的 model,是否在整個應用程式中強制則交由使用端決定。若使用端已經啟用強制,請不要從套件將其改回 requireMorphMap(false)。

先遷移既有資料再判斷相容性

引入別名註冊時,新安裝與既有應用程式升級的條件不同。官方文件也提醒,引入時需要將既有的完整限定類別名稱轉換為別名。

只確認反向取得是不夠的

嚴格模式停用時,只要舊 commentable_type 的類別仍然存在,有時仍可從評論以 morphTo() 取得文章。但文章端的 morphMany() 會依 getMorphClass() 的值篩選型別。註冊別名後它會尋找 acme-publishing.article,因此使用舊類別名稱的資料列會從文章的評論清單中消失。 MorphOneOrMany 在建立時保存型別名稱,並同時用於查詢條件與新建資料。即使新舊型別名稱混雜,也不會自動以 OR 搜尋所有值。

將升級步驟納入發布

以下是將舊類別名稱轉換為別名的程式片段。請將其實作為目標應用程式的資料遷移,不要從 service provider 自動執行。
僅靠這一步並無法完成部署。
  1. 調查目標 *_type 欄位與儲存值,在更新前準備好備份、筆數確認與還原步驟。若其他資料表也參照同一 model,也要一併納入。
  2. 首次引入時,要決定包含停止寫入或維護在內的切換步驟,避免寫入舊類別名稱的程序與寫入別名的程序同時存在。對於大型資料表,也要考慮更新鎖定與批次處理。
  3. 切換對應表與資料,在不改變主鍵值的前提下轉換型別名稱。遷移程式中的舊類別名稱應固定為過去實際儲存的字串,而不是會因未來重新命名而改變意義的 Article::class。
  4. 確認舊值的殘留筆數、新的儲存、雙向關聯取得與 Eager 載入,並在更新長時間執行的 worker 後再恢復寫入。
  5. 回滾時,要確認回退後的程式能否解析已儲存的別名。僅回退 Composer 版本,並無法還原資料庫中的型別名稱。
若維持已固定的別名,只變更 model 的命名空間,將對應表的值改為新類別即可沿用原本的型別名稱。另一方面,變更別名本身應視為伴隨資料遷移的變更。升級步驟也要與套件的版本相容性管理及套件 Migration 的公開與更新保持一致。

測試儲存值與雙向取得

在 Orchestra Testbench 中,不只驗證對應表,也要驗證存入資料庫的值與雙向取得。以下範例假設透過 Composer 自動載入套件,並已備妥 PDO 與 SQLite 驅動程式。Provider 在測試中明確註冊,與自動偵測的驗證分開。
對應表與強制旗標是彼此獨立的靜態狀態,因此不要以為只要重新建立應用程式就會初始化。本例對兩者都進行了保存與還原。在正式環境中也不要依請求或租戶改寫,而應僅限於啟動時的穩定註冊。已建立的關聯會保存型別名稱,因此中途變更註冊也不會一律反映到既有實例上。 另外,請在套件的 CI 中加入以下回歸案例。
  • 在使用端應用程式已啟用嚴格模式的狀態下註冊 provider,不會將其停用。
  • 在同一別名衝突、同一類別有多個別名、替換了具體 model 的情況下,已公開的儲存格式維持不變。
  • 準備以升級前類別名稱儲存的 fixture,遷移後舊值消失,雙向取得與筆數一致。
  • 包含佇列 worker 等在內的實際升級步驟,能以回退後的程式與資料組合進行還原。

參考的第一手資料

最後修改於 2026年11月11日