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() 中註冊別名。
$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 自動執行。- 調查目標
*_type欄位與儲存值,在更新前準備好備份、筆數確認與還原步驟。若其他資料表也參照同一 model,也要一併納入。 - 首次引入時,要決定包含停止寫入或維護在內的切換步驟,避免寫入舊類別名稱的程序與寫入別名的程序同時存在。對於大型資料表,也要考慮更新鎖定與批次處理。
- 切換對應表與資料,在不改變主鍵值的前提下轉換型別名稱。遷移程式中的舊類別名稱應固定為過去實際儲存的字串,而不是會因未來重新命名而改變意義的
Article::class。 - 確認舊值的殘留筆數、新的儲存、雙向關聯取得與 Eager 載入,並在更新長時間執行的 worker 後再恢復寫入。
- 回滾時,要確認回退後的程式能否解析已儲存的別名。僅回退 Composer 版本,並無法還原資料庫中的型別名稱。
測試儲存值與雙向取得
在 Orchestra Testbench 中,不只驗證對應表,也要驗證存入資料庫的值與雙向取得。以下範例假設透過 Composer 自動載入套件,並已備妥 PDO 與 SQLite 驅動程式。Provider 在測試中明確註冊,與自動偵測的驗證分開。- 在使用端應用程式已啟用嚴格模式的狀態下註冊 provider,不會將其停用。
- 在同一別名衝突、同一類別有多個別名、替換了具體 model 的情況下,已公開的儲存格式維持不變。
- 準備以升級前類別名稱儲存的 fixture,遷移後舊值消失,雙向取得與筆數一致。
- 包含佇列 worker 等在內的實際升級步驟,能以回退後的程式與資料組合進行還原。