A Blog
by Pim Veelders

Querying Eloquent morph relations

January 30, 2020
Laravel's Eloquent offers polymorphic relations which give you extra flexibility when modelling your data. Not only that, creating complex queries is a lot less painful.

Introduction

Laravel's Eloquent gives a great API to query your database. Instead of writing queries completely by hand you can leverage the strengths of Eloquent. Especially relationship queries are great imho. You define how your models relate to each other through relationships. Next, you can use them to build complex queries. In this post I want to describe how we can query MorphTo relations.

An example

Suppose we have two groups of people: students and teachers. We decide to put these in separate tables and create an associative table to link the two together. A student can have many teachers, a teacher can have many students. So far, we have these database tables:

teachers:
  id - integer
  name - string

students:
  id - integer
  name - string

student_teacher:
  student_id - integer
  teacher_id - integer

Provided we have setup our models (Teacher and Student) correctly using the BelongsToMany relationship we could write a query like this:

// Retrieve teachers without students
Teacher::whereDoesntHave('students')->get();

Now let's assume both teachers and students can create comments. For this we need a one-to-many polymorphic relation (similar to the example in the Laravel docs). So, we'll create one more table:

comments
  id - integer
  body - text
  commentable_id - integer
  commentable_type - string

The Comment class knows whether it's linked to a Teacher or a Student through the commentable_type attribute. This is the accompanying class:

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

    public function isFromTeacher()
    {
        return $this->commentable_type === 'App\Teacher';
    }
}

Querying polymorphic relations

Now that we have our world set-up, we can query our polymorphic relationship. To find comments from teachers without students we can write:

// Comments from teachers without students
Comment::whereHasMorph('commentable', ['App\Teacher'], function ($query) {
    $query->whereDoesntHave('students');
})->get();

The second argument of whereHasMorph determines which types should be included in the query. You can expand the array to include students:

// Comments from teachers or students with the name john
Comment::whereHasMorph('commentable', ['App\Teacher', 'App\Student'], function ($query) {
    // Both teachers and students have a "name" column
    $query->where('name', 'like', 'john%');
})->get();

When the morph query should differ per type you can use an extra argument ($type) and write custom query parts for each included type:

// Comments of: 
// - teachers with students with the name john, or
// - students with the name john
Comment::whereHasMorph('commentable', ['App\Teacher', 'App\Student'], function ($query, $type) {
    if ($type === 'App\Teacher') {
        $query->whereHas('students', function ($query) {
            $query->where('name', 'like', 'john%');
        });
    } else {
        $query->where('name', 'like', 'john%');
    }
})->get();

Eager loading

Sometimes we want to eager load relations. However, the results in the last example can contain comments from both students and teachers. Therefore, we will have to specify which relations to load per type:

// Load teacher's students with the name john
Comment::whereHasMorph('commentable', ['App\Teacher', 'App\Student'], function ($query, $type) {
    if ($type === 'App\Teacher') {
        $query->whereHas('students', function ($query) {
            $query->where('name', 'like', 'john%');
        });
    } else {
        $query->where('name', 'like', 'john%');
    }
})->with(['commentable' => function ($morphTo) {
    $morphTo->morphWith([
        'App\Teacher' => ['students' => function ($query) {
            $query->where('name', 'like', 'john%');
        }],
    ]);
}])->get();

We could also first get the collection, filter it to only contain teachers and then eager load the relations using loadMissing. Since the collection only contains Teacher objects, we can safely assume the students relatioship is present.

// Collection of comments
$comments->filter->isFromTeacher()->loadMissing(['commentable.students' => function ($query) {
    $query->where('name', 'like', 'john%');
}]);

Conclusion

You can build some pretty flexible and powerful queries using Eloquent morph relations :-)