跳轉到主要內容
想從套件為使用端應用程式的模型新增關聯時,可以透過 resolveRelationUsing() 註冊關聯的建立方式。不必直接改寫模型檔案,就能像一般關聯一樣進行取得、Eager Loading 以及依存在條件查詢。 本頁確認了 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 等,也請讓關聯欄位的型別一致。
註冊動態關聯並不會建立資料表或外鍵約束。這與套件 Migration 的發布與更新是不同的處理。下列提供者中也不包含 DB 存取或 Migration 的執行。

在 boot 中註冊建立方式

在 src/OrderNotesServiceProvider.php 中合併設定,並在 boot() 中為目標模型註冊回呼。
在使用端應用程式中發布設定,並將 order_model 改為 \App\Models\Order::class 等實際使用的具體模型。提供者的載入請透過自動探索設定。
hasMany() 中明確指定了外鍵 order_id 與父端的鍵。這是為了即使父模型的類別名稱是 Purchase,也不會讓框架推測為 purchase_id,而能與套件的資料表定義一致。
目標模型的設定由應用程式的管理者負責。請勿設計成從 HTTP 請求等直接接收類別名稱或關聯名稱,並在每次請求時重新註冊。此外,若註冊到 Eloquent 的基底 Model,將會影響其所有子孫模型。

像一般關聯一樣使用

對於已設定的訂單模型,呼叫方法會回傳 HasMany,存取屬性則會回傳取得結果的集合。
使用 create() 時,hasMany 會設定 order_id,因此不需要在使用者傳入的陣列中加入外鍵。反向的關聯不會自動新增,若有需要請另行定義。

回呼中不要執行到取得資料

若回呼的回傳值是 get() 的結果或單一模型,在存取屬性等情況下就無法作為有效的關聯處理。請將關聯的建構與資料取得分開。 Eager Loading 的 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') 等方式。
註冊位置不是服務容器,而是 static 屬性。若在閉包中擷取請求、已驗證使用者或租戶等,可能會在長時間執行的處理程序中被保留。請將註冊限定為啟動時穩定的定義,並在設定或程式碼變更時,一併更新佇列 Worker 或 Octane 等處理程序。另請參閱服務的生命週期設計。
同樣地,不要以為在 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 後的差異。
  • 在支援的 DB 中的欄位型別、索引、外鍵約束,以及使用者變更過的主鍵。

參考的第一手資料

最後修改於 2026年11月4日