Eager load hasMany & belongsTo (circular reference/infinite loop)
Asked Answered
P

2

10

UPDATE (SOLUTION)

  • If you need ->user relationship from one of the $image inside $user->images, then $user variable is already available cause you loaded the ->images from it!
  • Don't use protected $with Eloquent property. It's an anti-pattern.
  • Instead explicitly eager load relationships on-demand from where/when it's needed (Note: it should not prevent you to keep things DRY!)
  • If you really need/want to, see @nicksonyap answer. It does the trick (I believe – not tested).

ORIGINAL

I'm running into what I believe is a simple problem:

  • I have a User object that has many Images
  • Image belongs to User... (inverse relation)

My problem is that I want to eager load both the images() on the User model and the user() on the Image model. To do so, I just setup a $with property as explained in the docs.

My User model:

class User extends EloquentModel {
    protected $with = ['images'];

    public function images()
    {
        return $this->hasMany(Image::class);
    }
}

My Image model:

class Image extends EloquentModel {
    protected $with = ['user'];

    public function user()
    {
        return $this->belongsTo(User::class);
    }
}

But when performing:

$user = User::find(203);

This results in an infinite loop (php segmentation fault). There must be some kind of circular reference that I am not able to locate:

[1]    85728 segmentation fault

EDIT 2016/02

This is the simplest "Workaround" I found:

// User.php
public function setRelation($relation, $value)
{
    if ($relation === 'images') {
        foreach ($value as $image) {
            $image->setUser($this);
        }
    }
    return parent::setRelation($relation, $value);
}
Peso answered 25/1, 2016 at 14:38 Comment(0)
I
20

There is a without() method: https://laravel.com/api/5.8/Illuminate/Database/Eloquent/Builder.html#method_without

Placing without() on both sides of a relationship worked.

class Property extends EloquentModel {
    protected $with = ['images'];

    public function images()
    {
        return $this->hasMany(Image::class)->without('property');
    }
}
class Image extends EloquentModel {
    protected $with = ['property'];

    public function property()
    {
        return $this->belongsTo(Property::class)->without('images');
    }

    public function getAlt()
    {
        return $this->property->title;
    }
}

UPDATE:

Even though using without() easily avoid the infinite loop issue, through years of experience with Laravel I realize it is bad practice to set $with in the model as it causes relationships to always load. Hence leading to circular reference/infinite loop

Rather, always use with() to explicitly specify necessary relationships to be eager loaded, however deep necessary (relationship of relationship)

For example:

$user = User::with('images' => function ($query) {
            $query->with('property' => function ($query) {
                $query->with('deeperifneeded' => function ($query) {
                    //...
                });
            });
        ]);

Note: May need to remove without()

Inwards answered 18/10, 2018 at 18:15 Comment(1)
That could work. I accepted your answer (~3 years later...), although I would advise against it in favor of explicitness (see my update).Peso
P
1

When you try to find a Property, that property eager loads all the images it has and every Image eager loads the property it belongs to, which is the property you try to find, which will again start to eager load all the images it has. etc...

The way I would resolve this problem is by not eager loading inside the models, but by eager loading when calling the models.

so using the following:

$prop = Property::with('images')->find(203);

while removing this line in the Property model:

protected $with = ['images'];

And this line in the Image model:

protected $with = ['property'];

I hope this solution works for you.

Pyxie answered 28/1, 2016 at 14:3 Comment(1)
Hi, thx. I feel you, I'm just surprised there is no built-in way to handle that case in Eloquent. From my understanding that would be as easy as when Property receives images relationship, then for each image ->setRelation('property', $this). Instead of re-asking the same Property to the ORM, etc, etc... I'm surprised there is nothing preventing you to avoid that infinite Loop (error message???)Peso

© 2022 - 2024 — McMap. All rights reserved.