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 を推測させず、パッケージのテーブル定義と一致させるためです。
通常のリレーションと同じように使う
設定した注文モデルに対して、メソッド呼び出しは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') などを使います。
同様に、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での列型・インデックス・外部キー制約と、利用者が変更した主キー。