背景
Laravel Laravel

【Laravel】動的にリレーションを設定する際に逆方向のリレーションも同時に設定する方法

こんにちは、スズキです。

今回はLaravelでモデルのリレーションを動的に設定する方法を紹介します。

流れとしては以下の順に解説します。
  1. 一方向のリレーションを動的に設定する方法
  2. 双方向のリレーションを動的に設定する方法
  3. 双方向のリレーションを動的に設定する方法 別解
ちなみに、メインで紹介したいのは3番目の別解の部分であり、残り2つは別解を紹介するための前ふりのための解説です。
 

方法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化の際などに)無限ループに陥るみたいなので注意してください。
≪ [PHP]高品質コンポーネント群、Aura for PHP  |  baserCMS用のLaravel Valetカスタムドライバを書いた ≫

Web制作のお問い合わせ

075-744-6842

(平日/土曜 10:00~17:00)

 お問い合わせ