【Laravel】動的にリレーションを設定する際に逆方向のリレーションも同時に設定する方法
こんにちは、スズキです。
今回はLaravelでモデルのリレーションを動的に設定する方法を紹介します。
流れとしては以下の順に解説します。
以下のような構造のモデルクラス「Parent」「Child」が存在するとします。
●Parentモデル
●Childモデル
このとき、例えば、ParentインスンタンスにChildインスタンスのリレーションを動的に設定するには以下のようにすると実現できます。
(setRelationメソッドを呼び出している箇所がリレーションを設定している処理になります)
方法1で紹介した方法では
「parent => child」方向のリレーションは設定していますが、「child => parent」方向のリレーションは設定していません。
そのため、「$parent->child->parent->name」のようなアクセスの仕方をするとエラーになります。
「parent => child」「child => parent」の両方向にリレーションを設定するには以下のようにすることで実現できます。
見て分かる通り、双方向の場合は親側、子側のどちらにもsetRelationを設定すれば良いです。
はい、ではここからが本題です。
実は開発作業をしていて、
しかし、該当箇所が多かったため、setRelationを利用した箇所に対し「該当箇所の洗い出し→該当箇所を修正」という作業をできるだけしたくなかったわけです。
そこで思いました。
そこで試行錯誤してみたところ、モデルクラスを修正することで実現が可能でした。
以下のように、モデルクラスのsetRelationメソッドを上書きすることで実現できます。
ちなみに、上記ソースのsetRelationの部分で
このチェックを行わずに直に
今回はLaravelでモデルのリレーションを動的に設定する方法を紹介します。
流れとしては以下の順に解説します。
- 一方向のリレーションを動的に設定する方法
- 双方向のリレーションを動的に設定する方法
- 双方向のリレーションを動的に設定する方法 別解
方法1 一方向のリレーションを動的に設定する方法
以下のような構造のモデルクラス「Parent」「Child」が存在するとします。
●Parentモデル
class Parent extends Model { public function child() { return $this->hasOne(Child::class, 'parent_id'); } }
●Childモデル
class Child extends Model { public function parent() { return $this->belongsTo(Parent::class, 'parent_id'); } }
このとき、例えば、ParentインスンタンスにChildインスタンスのリレーションを動的に設定するには以下のようにすると実現できます。
(setRelationメソッドを呼び出している箇所がリレーションを設定している処理になります)
// Parent情報取得 $parent = Parent::query()->first(); // Childのレコード情報が登録されていない場合 if(!$parent->child) { // 空のChild情報を設定 $emptyChild = new Child(); $parent->setRelation('child', $emptyChild); } // レコード情報が無い場合には空のリレーションを設定しているので、 // 本来は「$parent->child === null」だったとしても、 // 以下の処理の際にエラーにならない $childName = $parent->child->name;
方法2 双方向のリレーションを動的に設定する方法
方法1で紹介した方法では
「parent => child」方向のリレーションは設定していますが、「child => parent」方向のリレーションは設定していません。
そのため、「$parent->child->parent->name」のようなアクセスの仕方をするとエラーになります。
「parent => child」「child => parent」の両方向にリレーションを設定するには以下のようにすることで実現できます。
// Parent情報取得 $parent = Parent::query()->first(); // Childのレコード情報が登録されていない場合 if(!$parent->child) { // 空のChild情報を設定 $emptyChild = new Child(); $emptyChild->setRelation('parent', $parent); $parent->setRelation('child', $emptyChild); } // 双方向にリレーションを設定しているので、 // 本来は「$parent->child === null」だったとしても、 // 以下の処理の際にエラーにならない $parentName = $parent->child->parent->name;
見て分かる通り、双方向の場合は親側、子側のどちらにもsetRelationを設定すれば良いです。
方法3 双方向のリレーションを動的に設定する方法 別解
はい、ではここからが本題です。
実は開発作業をしていて、
$emptyChild = new Child(); $parent->setRelation('child', $emptyChild);という一方向リレーションを
$emptyChild = new Child(); $emptyChild->setRelation('parent', $parent); $parent->setRelation('child', $emptyChild);という双方向リレーションに直さないといけないことが判明しました。
しかし、該当箇所が多かったため、setRelationを利用した箇所に対し「該当箇所の洗い出し→該当箇所を修正」という作業をできるだけしたくなかったわけです。
そこで思いました。
$emptyChild = new Child(); $parent->setRelation('child', $emptyChild);という記述だけで双方向のリレーションが設定できないものかと。
そこで試行錯誤してみたところ、モデルクラスを修正することで実現が可能でした。
$parent->setRelation('child', $emptyChild);の記述だけで「parent => child」「child => parent」の双方向リレーションを設定するには、
以下のように、モデルクラスのsetRelationメソッドを上書きすることで実現できます。
class Parent extends Model { /** * Set the specific relationship in the model. * * @param string $relation * @param mixed $value * @return $this */ public function setRelation($relation, $value) { $isTargetSetRelation = $this->isTargetSetRelation($value); if ($isTargetSetRelation) { /** * @var Model $value */ if (!$value->parent) { $value->setRelation('parent', $this); } } return parent::setRelation($relation, $value); } /** * @param $value * @return bool */ private function isTargetSetRelation($value) { if ($value instanceof Child) { return true; } return false; } public function child() { return $this->hasOne(Child::class, 'parent_id'); } }
ちなみに、上記ソースのsetRelationの部分で
if (!$value->parent) { $value->setRelation('parent', $this); }というように、リレーションがあるかをチェックしてから逆方向のリレーションを設定していますが、
このチェックを行わずに直に
$value->setRelation('parent', $this);としてしまうと、状況によってはtoArray利用時に(Json化の際などに)無限ループに陥るみたいなので注意してください。