【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化の際などに)無限ループに陥るみたいなので注意してください。