メインコンテンツへスキップ
パッケージから利用アプリケーションのモデルに関連を追加するには、resolveRelationUsing() でリレーションの生成方法を登録できます。モデルファイルを直接書き換えずに、通常のリレーションと同じように取得・Eagerロード・存在条件による検索を利用できます。 このページは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などを使う場合は、関連する列の型も一致させます。
動的リレーションの登録は、テーブルや外部キー制約を作成しません。パッケージのマイグレーション公開と更新とは別の処理です。以下のプロバイダーにもDBアクセスやマイグレーション実行を含めません。

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ロードの Builder::getRelation() は、新しいモデルインスタンスで関連を構築します。したがって、コールバックが「取得済みの注文IDがあること」を前提にしたり、そのIDに応じて別の関連定義を返したりする設計は避けます。 動的な登録でもN+1問題やLazy Loadingの制約はなくなりません。複数モデルを扱うときは、通常どおり with() などで関連を読み込みます。

belongsToでは関連名の推測にも注意する

belongsTo() は関連名を省略すると呼び出し元をバックトレースで推測します。クロージャからの定義では通常のモデルメソッドと同じ推測に頼らず、外部キー・所有者キーに加えて第4引数の関連名も明示できます。 次は、注文側に customer_id、顧客側に id がある場合の独立した例です。

名前衝突と継承の規則を理解する

HasRelationships は、モデルのクラス名と関連名をキーにクロージャをstaticプロパティへ保存します。存在しないメソッドの呼び出しを受けた Model::__call() がresolverを検索し、対象のモデルインスタンスを渡して実行します。 図はresolverが見つかる場合の経路です。既存メソッドや属性との衝突では、別の経路になる点に注意します。 関連名には acmeNotes のような固有の名前を使い、利用者のメソッド・属性・ほかのパッケージとの衝突を確認します。method_exists() だけでは動的な登録の存在を確認できません。Eloquentの isRelation() はresolverも確認しますが、属性との衝突がないことまでは保証しません。

登録状態と取得結果の寿命を分ける

resolverの登録と、各モデルが保持するロード済みの関連データは別物です。 すでに $order->acmeNotes を読み込んだ後でresolverを上書きしても、そのモデルが保持する結果は自動更新されません。再取得が必要なら、関連結果を取り除いてから load('acmeNotes') などを使います。
登録先はサービスコンテナではなくstaticプロパティです。クロージャにリクエスト・認証ユーザー・テナントなどを取り込むと、長寿命プロセスで保持される可能性があります。登録は起動時の安定した定義に限定し、設定やコードの変更時はキューワーカーやOctaneなどのプロセスも更新します。サービスのライフサイクル設計も参照してください。
同様に、Testbenchでアプリケーションを作り直すだけでresolverが消えるとは考えません。有効・無効を比較するテストには別のモデルfixtureを使うか、PHPUnitのプロセス分離を使い、前の登録への依存を避けます。

登録とSQLの両方をテストする

Orchestra Testbenchでは、プロバイダーの登録だけでなく、外部キーと実際の関連取得を検証します。次は tests/DynamicRelationsTest.php の例です。パッケージのクラスとテストの名前空間をComposerで読み込めるようにし、PDO・SQLiteドライバーを用意します。
テスト用モデル名は OrderFixture ですが、外部キーは order_fixture_id ではなく order_id のままです。クラス名からの推測に依存していないことを確認できます。 パッケージのCIでは、さらに次の境界も検証します。
  • 未登録の別fixtureで、order_model が null なら関連を追加しないこと。
  • 子モデルへの継承と、子だけに同名のresolverを登録した場合の優先順位。
  • 既存のメソッドや属性と衝突した場合に、意図したAPIが利用できること。
  • 関連を一度ロードしたモデルと、新しいモデルでのresolver再登録後の違い。
  • 対応DBでの列型・インデックス・外部キー制約と、利用者が変更した主キー。

参照した一次情報

最終更新日 2026年11月4日