Framework: [5.4] MorphTo eager load issue

Created on 30 Nov 2016  路  14Comments  路  Source: laravel/framework

  • Laravel Version: 5.2.45
  • PHP Version: 7.0.13

Description:

Recently updated a production app from Laravel v5.2.32 to 5.2.45 (latest), and polymorphic eager loads have stopped working. Any help would be much appreciated!

I get the following error:

Call to undefined method Illuminate\Database\Query\Builder::route()

From debugging, the problem seems to be https://github.com/laravel/framework/blob/5.2/src/Illuminate/Database/Eloquent/Relations/MorphTo.php#L188

If i change

    $query = $this->replayMacros($instance->newQuery())
        ->mergeModelDefinedRelationConstraints($this->getQuery())
        ->with($this->getQuery()->getEagerLoads());

to

    $query = $this->replayMacros($instance->newQuery())
        ->mergeModelDefinedRelationConstraints($this->getQuery())
        ->with($instance->newQuery()->getEagerLoads());

it fixed the problem, but I don't want to submit a PR in-case that is not actually a general fix.

Steps To Reproduce:

My Log model has two polymorphic relations defined - primary and secondary. When loading primary, the Journey and Achievement models are being correctly loaded, however route is being eager loaded for both of the polymorphic relations, even though it is only defined as the $with parameter for Journey.

Log.php

class Log extends Model
{
    public function primary()
    {
        return $this->morphTo();
    }

    public function secondary()
    {
        return $this->morphTo();
    }

Achievement.php

class Achievement extends Model
{

}

Journey.php

class Journey extends Model 
{

    protected $with = ['route'];

    public function route()
    {
        return $this->belongsTo(Route::class)->withTrashed();
    }

}

Thanks.

bug

Most helpful comment

This issue can probably be summed up as "Polymorphic eager load will try to cross eager load from different polymorphic models, causing a RelationNotFoundException exception.

I actually came up with a workaround for another project where I ran into this issue. The workaround is:

$results = $results->groupBy('model_type')
    ->map(function($group) {
        $group->load(['model']);
        return $group;
    })->flatten();

Where model is the polymorphic relation. I still get eager-loading, but by grouping the polymorphic results before eager loading, I avoid running into the cross eager load issue. I wonder if this approach might work baked into Eloquent somehow?

All 14 comments

5.2 is not supported anymore, can you replicate the problem on a 5.3 installation?

@themsaid Thanks for the reply.

Yes - I updated the app to Laravel 5.3 and replicated the same issue:

RelationNotFoundException in RelationNotFoundException.php line 20:
Call to undefined relationship [route] on model [Walking\Achievements\Achievement].

Do you know if changing $this->getQuery()->getEagerLoads() to $instance->newQuery()->getEagerLoads() would work, or is that likely to break the intended update from https://github.com/laravel/framework/commit/ee7b8cffb046467d220439b940f983f5dde89d6c ?

Laravel Version: 5.4.9
PHP Version: 7.1.0

+1, I've stumbled onto exactly same issue in 5.3, upgraded to 5.4 and it is still reproducible. Looks like ee7b8cffb046467d220439b940f983f5dde89d6c fixes some other (similar) case.

In 5.4 the illness is now here:
https://github.com/laravel/framework/blob/5.4/src/Illuminate/Database/Eloquent/Concerns/HasRelationships.php#L183

I didn't dig much, but when I replaced $instance->newQuery() with $instance->newQuery()->setEagerLoads([]) (as in ee7b8cffb046467d220439b940f983f5dde89d6c) the issue disappeared. Not sure how it affects eager loading.

Let me know if you need some sandbox with a reproduce of this issue.

I just came across this issue in 5.4 as well. Have a model PurchaseOrder with purchaseOrderable() method that contains morphTo().

If any of the related models have $with defined, the framework will try and eager load relationships on all objects using the all the keys defined across different models.

E.g.

PurchaseOrder

Model A

  • with = ['parentA', 'parentB']

Model B

  • with = ['parentC', 'parentD']

Will try and load parent A,B,C,D for all models.

This issue can probably be summed up as "Polymorphic eager load will try to cross eager load from different polymorphic models, causing a RelationNotFoundException exception.

I actually came up with a workaround for another project where I ran into this issue. The workaround is:

$results = $results->groupBy('model_type')
    ->map(function($group) {
        $group->load(['model']);
        return $group;
    })->flatten();

Where model is the polymorphic relation. I still get eager-loading, but by grouping the polymorphic results before eager loading, I avoid running into the cross eager load issue. I wonder if this approach might work baked into Eloquent somehow?

Spot on! Thanks for adding this @davidrushton, a great solution in the mean time.

Is this still an issue?

@staudenmeir I've not tested on 5.5 or 5.6, but my 5.4 app (5.4.9) still has this issue.

@davidrushton I was asking because I couldn't even reproduce the issue on 5.4.0. Is your example still valid?

@staudenmeir
As of Laravel 5.4.9 it was reproducible for me (see above).
But I haven't checked in 5.5 or 5.6.
(probably a failing test needs to be added to not guess)

I'm using the documentation example and it works on 5.4.0:

class Comment extends Model {
    public function commentable() {
        return $this->morphTo();
    }
}

class Post extends Model {
    protected $with = ['user'];

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

class Video extends Model {}

$post = Post::create(['user_id' => 1]);
$video = Video::create();
Comment::create(['commentable_type' => Post::class, 'commentable_id' => $post->id]);
Comment::create(['commentable_type' => Video::class, 'commentable_id' => $video->id]);

Comment::with('commentable')->get();

My specific example that still isn't working on 5.4.9 is:


class Index extends Model {

    public function model()
    {
        return $this->morphTo('model');
    }

}

class Product extends Model {

    protected $with = ['prices', 'thumbnail', 'translations'];

}

class Page extends Model {

    protected $with = ['translations'];

}

When I effectively do

$results = Index::get();
$results->load('model');

I get this exception:

RelationNotFoundException
Call to undefined relationship [thumbnail] on model [Page].

But when I group the load as so it works:

$results = Index::get();
$results = $results->groupBy('model_type')
    ->map(function ($group) {
        $group->load('model');
        return $group;
    })->flatten();

I see, it only happens on lazy loading:

Comment::all()->load('commentable');

It's still an issue on 5.6.

@staudenmeir Thanks for checking 5.6.

I have to use lazy loading since i'm actually using Laravel Scout to fetch the Index results, and that has no with method on the builder.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

shopblocks picture shopblocks  路  3Comments

Anahkiasen picture Anahkiasen  路  3Comments

ghost picture ghost  路  3Comments

lzp819739483 picture lzp819739483  路  3Comments

gabriellimo picture gabriellimo  路  3Comments