Be part of JetBrains PHPverse 2026 on June 9 – a free online event bringing PHP devs worldwide together.

rudexpunx's avatar

Eager load relationship if exists

I use a polymorphic relationship, where the subject (morphTo()) may have various relationships (so they are not the same on each one of them). Their existence depends on morphed model.

I need to retrieve related models. whereHas() won't help, because ones without particular relationship would be missing.

with() won't work either, because given relationship doesn't always exist, so it would throw an error on models where the relationship isn't defined.

Now, I am almost sure there is now way around it, because when you think about it, Laravel doesn't know of what type the morphed object is. That information is stored in the DB (object is defined by [whatever]able_type and [whatever]able_id). So the only way to find out is to look it up in DB. And that's a query. But that's maybe just my limited mind, and I know you guys probably know better. It wouldn't be my first time with Laravel when I thought it's impossible/beyond framework's edge, just to find out it's perfectly possible and even pretty common + well documented :).

Here's some example code for better understanding:

<?php

...

class Post extends Model
{
    /**
     * Get all of the post's comments.
     */
    public function comments()
    {
        return $this->morphMany('App\Comment', 'commentable');
    }

    /**
     * This relationship is available for Post model only
     */
    public function relationA()
    {
        // return $this->hasMany(...);
    }
}

class Video extends Model
{
    /**
     * Get all of the video's comments.
     */
    public function comments()
    {
        return $this->morphMany('App\Comment', 'commentable');
    }

    /**
     * This relationship is available for Video model only
     */
    public function relationB()
    {
        // return $this->hasMany(...);
    }
}

class Comment extends Model
{
    /**
     * Get all of the owning commentable models.
     */
    public function commentable()
    {
        return $this->morphTo();
    }

    public static function feed()
    {
        return self::with('commentable')
            ->withIfExists(['commentable.relationA', 'commentable.relationB'])
            // ...
            ->get();
    }

    public function scopeWithIfExists($query, $relations = [])
    {
        // There is likely no way to implement such a scope
        // in order to reduce number of queries by eager loading relations
        // as we don't know of what type the subject is
        // without looking it up in database

        // I'll be very thankful if you could prove I am wrong
    }
}
0 likes
11 replies
36864's avatar

https://laravel.com/docs/5.5/eloquent-relationships#constraining-eager-loads

You can easily constrain eager loading to get the result you need.

//Comment.php
...
public function feed()
{
    return self::with(['commentable.relationA' => public function($query){
        $query->where('commentable_type', 'App\Post');
        },
        'commentable.relationB' => public function($query){
        $query->where('commentable_type', 'App\Video');
        }])
        ->get();
}
...

Alternatively, you could rename the relations you want for each model to be the same, or more realistically, add new relations for your specific use case. Something like feedRelation which for one model will return relationA and for the other will return relationB:

//Video.php
...
public function feedRelation()
{
    return $this->belongsTo(A::class, 'a_id');
}
...

//Post.php
...
public function feedRelation()
{
    return $this->belongsTo(B::class, 'b_id');
}
...

//Comment.php
...
public function feed()
{
    return self::with('commentable.feedRelation')
            ...
            ->get();
}
...
rudexpunx's avatar

These approaches do not work. With the first one, the error is the same as without the constraint: Call to undefined relationship ..., so it doesn't filter it at all.

To be honest, I tried your second suggestion even before I posted here, because it came to my mind like a little dirty, but must work hack. However, it did not work. The issue is, that once you wrap your relation method in another method, it returns null. No idea why it works like that, but I still think there is now way around it. Unless there is of course :)

36864's avatar

Sorry, I was under the impression the first approach would restrict loading based on the first relation, but it doesn't.

The second method should work just fine, what do you mean by wrapping the function? In the code sample you posted earlier, your feed() function didn't have a return statement. Did you add that?

Here's a third solution, using lazy eager loading.

//Comment.php
...
public function feed()
{
    $feed = self::with('commentable')->get();
    $feed->where('commentable_type', 'App\Post')->load('relationA');
    $feed->where('commentable_type', 'App\Video')->load('relationB');
    return $feed;
}
...
1 like
rudexpunx's avatar

load() is defined on the Model class, not on the Builder class, so you can't call load on builder. Calling $feed->load() leads to Call to undefined method Illuminate\Database\Query\Builder::load().

And yes, my actual feed method returns the $feed collection, but it doesn't work. To make it work, you need to wrap relation methods on models by a wrapper method, which has the same name, right? (because relation methods do not have same names). And once you do that, wrapper returns null. Didn't have much time to find out why.

<?php

namespace Illuminate\Database\Eloquent;

// ...

/**
 * @mixin \Illuminate\Database\Eloquent\Builder
 * @mixin \Illuminate\Database\Query\Builder
 */
abstract class Model implements ArrayAccess, Arrayable, Jsonable, JsonSerializable, QueueableEntity, UrlRoutable
{

// ...

/**
     * Eager load relations on the model.
     *
     * @param  array|string  $relations
     * @return $this
     */
    public function load($relations)
    {
        $query = $this->newQuery()->with(
            is_string($relations) ? func_get_args() : $relations
        );

        $query->eagerLoadRelations([$this]);

        return $this;
    }

// ...
36864's avatar

My bad, put the ->get() on the wrong line. I edited my previous post.

I still don't quite understand what you mean with the whole wrapping business. Post some code.

rudexpunx's avatar

This solution if fully functional

//Comment.php
...
public function feed()
{
    $feed = self::with('commentable')->get();
    $feed->where('commentable_type', 'App\Post')->load('commentable.relationA');
    $feed->where('commentable_type', 'App\Video')->load('commentable.relationB');
    return $feed;
}
...

however, it doesn't reduce number of queries at all, so it has the same effect as:

//Comment.php
...
public function feed()
{
    return self::with('commentable')->get();
}
...
Parasoul's avatar

You can declare your relation this way :

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

public function posts()
{
    return $this->belongsTo('App\Post')->where('commentable_type', 'App\Post')->with('RelationA');
}

public function videos()
{
    return $this->belongsTo('App\Video')->where('commentable_type', 'App\Video')->with('RelationB');
}

//And if you really need them with one attribut
public function getLoadedCommentableAttribute()
{
    return $this->posts ?: $this->videos;
}
1 like
rudexpunx's avatar

@Parasoul nope, it's morphTo() and morphMany(), so polymorphic one to many (video or a post may have many comments)...

bra_lat's avatar

I know this is old but @orest's answer is exactly what I was looking for

1 like

Please or to participate in this conversation.