phplaraveleloquentlaravel-8database-relations

How to insert existing model into relation


I often have two models in a 1:x relationship:

class Child extends Model 
{
  public function parent() 
  {
    return $this->belongsTo(Parent::class);
  }
}

Now there are times when I already have an instance of one of the related models within a function:

function f(Parent $parent): Child
{
  // ...
  $child = $parent->createChild();
  // ...
  return $child
}

A different function takes Child as a parameter and requires an attribute of the Parent class:

function g(Child $child)
{
  $child->update([
    'attribute' => h($child->parent->attribute)  
  ]);
}

Now if g is called with the Child instance returned from f, then the Parent instance will be retrieved from the DB twice. Unless we add a Parent parameter and pass in the instance to g, or alternatively modify f as follows:

function f(Parent $parent): Child
{
  // ...
  $child = $parent->createChild();
  // ...
  $child->parent = $parent;
  return $child
}

I prefer this second approach to adding another parameter because the context from which g is called might not have access to the Parent instance. And it often works fine. However, it fails on the update statement in g:

SQLSTATE[42S22]: Column not found: 1054 Unknown column 'parent' in 'field list' (SQL: update `children`...

So my question is:

Is there a way to insert an existing model instance into the appropriate relation without the model registering the assignment as a column update?


Solution

  • Calling $child->parent = $parent attempts to set a parent field attribute on the $child object. However, you want to set the parent relationship attribute. In order to do this, you need to use the setRelation() method:

    function f(Parent $parent): Child
    {
        // ...
        $child = $parent->createChild();
        // ...
        $child->setRelation('parent', $parent);
        return $child;
    }
    

    At this point, however, you will want to be careful that you're not creating a circular reference. If the $parent->child relationship is set to the $child instance, and then you set the $child->parent relationship to the $parent instance, you've created a circular reference that will blow up if you attempt to convert either instance to json (or an array).

    That is, if $parent->child->parent === $parent, then a circular reference exists, and creating the json/array for that will be an infinite loop.

    In this case, if you want to be extra careful, you can use the withoutRelations() method to assign the relationship to a clone of the parent, which will not have any relationships loaded.

    function f(Parent $parent): Child
    {
        // ...
        $child = $parent->createChild();
        // ...
        $child->setRelation('parent', $parent->withoutRelations());
        return $child;
    }
    

    After this, then $parent->is($parent->child->parent) will be true, but $parent->child->parent === $parent will be false, so no circular reference.