v13.42.0 の公式実装を確認しています。リレーションの基本はEloquentリレーション入門、利用者のモデルに外から関連を追加する方法は動的リレーション登録を参照してください。
型名と関連名を分ける
多態リレーションは、関連先の主キーと型名を組み合わせて、複数種類のモデルを参照します。たとえばコメントの関連先を記事や動画にできる構成では、commentable_id だけでなく commentable_type も必要です。
未登録の場合、通常は
Acme\Publishing\Models\Article のような完全修飾クラス名が型として使われます。別名を登録すると、新しい関連の作成などでは acme-publishing.article が使われます。既存行の値は登録だけでは書き換わりません。
Morph mapは関連メソッドやテーブルを作成する機能ではありません。モデル・列・インデックスは、通常どおり用意します。
パッケージ固有の別名だけを登録する
例の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などを使う構成では関連先と列型を一致させてください。
次に、通常の非遅延サービスプロバイダー src/PublishingServiceProvider.php の boot() で別名を登録します。
$alias は acme-publishing.article、$class は Article::class になります。Comment 自身が別の多態リレーションの関連先にならない限り、この例のために Comment の別名まで登録する必要はありません。
マージと名前衝突の規則を理解する
Relation::morphMap() はstaticの対応表を操作します。デフォルトのマージでは、新しい配列と既存の配列を $map + static::$morphMap で結合します。
パッケージから第2引数に
false を渡すと、ほかのパッケージや利用アプリの登録を消してしまいます。通常の登録では既存表を維持し、article のような汎用名ではなく、配布元・パッケージを識別できる別名を選びます。同じ別名を異なる意味に再利用しないでください。
クラス名のリストだけを渡す形式では、Laravelは各モデルを生成してテーブル名をキーにします。保存形式をテーブル名の変更からも切り離したいパッケージでは、上の例のように別名とクラスの対応を明示します。
標準の getMorphClass() は具象モデルのクラスを探します。Article のサブクラスを利用しても、親クラスの別名が自動的に継承されるわけではありません。モデル差し替えに対応するパッケージでは、実際に使うクラスを対応表へ登録する設計が必要です。
厳格モードは利用アプリ全体の方針にする
公式ドキュメントのenforceMorphMap() は、別名の登録と requireMorphMap(true) をまとめて行います。パッケージだけに限定した強制ではありません。
v13.42.0 の標準実装では、厳格モードは次の両方向に影響します。
- モデルから型名へ:
getMorphClass()は未登録モデルに対してClassMorphViolationExceptionを投げます。組み込みの汎用Pivotクラスには例外扱いがありますが、通常のモデルや独自Pivotに同じ免除はありません。 - 保存された型名からクラスへ:
getActualClassNameForMorph()は対応表のキーにない値に対して同じ例外を投げます。これはmorphTo()の通常取得とEagerロードでも使われます。対応表の「値」にクラスがあっても、古い完全修飾クラス名が「キー」にないなら読み込めません。
morphMap() で自分のモデルだけを登録し、アプリ全体で強制するかは利用者に委ねます。利用者がすでに強制している場合、パッケージから requireMorphMap(false) に戻さないでください。
既存データを移行してから互換性を判断する
別名登録の導入は、新規インストールと既存アプリの更新で条件が異なります。公式ドキュメントも、導入時に既存の完全修飾クラス名を別名へ変換する必要があると注意しています。逆向きの取得だけでは確認が足りない
厳格モードが無効なら、古いcommentable_type のクラスが存在する間は、コメントから morphTo() で記事を取得できる場合があります。しかし記事側の morphMany() は getMorphClass() の値で型を絞り込みます。別名の登録後は acme-publishing.article を探すため、古いクラス名の行は記事側のコメント一覧から外れます。
MorphOneOrMany は生成時に型名を保持し、取得条件と新規作成の両方に使います。新旧の型名が混在しても、すべての値を自動的にOR検索するわけではありません。
更新手順をリリースに含める
次は、旧クラス名を別名へ変換する処理の断片です。対象アプリのデータ移行として実装し、サービスプロバイダーから自動実行しないでください。- 対象の
*_type列と保存値を調査し、更新前のバックアップ・件数確認・復元手順を用意します。別のテーブルから同じモデルを参照している場合は、そちらも対象です。 - 最初の導入では、旧クラス名を書くプロセスと別名を書くプロセスが混在しないよう、書き込み停止やメンテナンスを含む切り替え手順を決めます。大きなテーブルでは更新ロックやバッチ処理も検討します。
- 対応表とデータを切り替え、主キーの値は変えずに型名を変換します。移行コードの旧クラス名は、将来の改名で意味が変わる
Article::classではなく、過去に実際に保存された文字列として固定します。 - 旧値の残存件数、新規保存、両方向の関連取得、Eagerロードを確認し、長寿命ワーカーを更新してから書き込みを再開します。
- ロールバックする場合は、戻すコードが保存済みの別名を解決できるか確認します。Composerのバージョンを戻すだけでは、DBの型名は戻りません。
保存値と両方向の取得をテストする
Orchestra Testbenchでは、対応表だけでなくDBに保存した値と両方向の取得を検証します。次の例はComposerでパッケージをオートロードし、PDO・SQLiteドライバーを用意した構成です。プロバイダーをテスト内で明示的に登録し、自動検出の検証とは分けています。- 利用アプリがすでに厳格モードを有効にしている状態で、プロバイダーを登録しても無効化しないこと。
- 同じ別名の競合、同じクラスへの複数の別名、差し替えた具象モデルで、公開した保存形式が変わらないこと。
- 更新前のクラス名で保存したfixtureを用意し、移行後に旧値がなくなり、両方向の取得と件数が一致すること。
- キューワーカーなどを含む実際の更新手順と、戻すコード・データの組み合わせで復元できること。