Laravel Has Many Through Pivot Relationships

May 22nd, 2024

Laravel doesn't support has many through pivot relationships out the box, but we can still achieve this manually with a pivot model.

Let's take a practical example directly from the Codecourse codebase. We have three models.

  • Course
  • Episode
  • Path

A Course has many Episodes and a Path has many Courses, but a course can belong to more than one path, so it's defined as a many-to-many relationship.

Eloquent has a hasManyThrough relationship, but if we were to try and find all Episodes within a Path, the following relationship wouldn't work.

class Path extends Model
{
    public function episodes()
    {
        return $this->hasManyThrough(Episode::class, Course::class);
    }
}

That's because Eloquent will attempt to find a path_id column on the courses table (it's in the course_path pivot table).

To solve this, create a pivot model for the pivot table you're working with.

php artisan make:model --pivot CoursePath

This will generate the following class.

class CoursePath extends Pivot
{
    //
}

We don't need to add any relationships to this pivot model; it'll be enough to allow us to define a has many through relationship using this pivot model.

Now we have our pivot model, let's define a hasManyThrough relationship using this pivot model.

class Path extends Model
{
    public function episodes()
    {
        return $this->hasManyThrough(
            Episode::class,
            CoursePath::class,
            'path_id'
            'course_id',
            'id',
            'course_id'
        );
    }
}

Notice that instead of having many Episodes through Courses, we now have many Episodes through our CoursePath pivot model. Let's break this down even further to explain what's happening.

  • The third argument, path_id, references the path_id column on the course_path pivot table.
  • The fourth argument, course_id, references the course_id on the episodes table.
  • The fifth argument, id, references the id on the paths table.
  • The final argument, course_id, references the course_id column on the course_path pivot table.

This is how hasManyThrough is defined in the Laravel codebase.

public function hasManyThrough($related, $through, $firstKey = null, $secondKey = null, $localKey = null, $secondLocalKey = null)
{
    //
}

With this new relationship defined, we now have access to all distant Episodes belonging to a Path!

Here's an example of how I use this in the Codecourse codebase to total up the duration of all episodes within all courses within a path.

class Path extends Model
{
    protected static function booted()
    {
        static::addGlobalScope(function ($query) {
            $query
                ->withSum([
                    'episodes as duration_seconds' => function ($query) {
                        $query->where('upcoming', false);
                    }
                ], 'duration');
        });
    }
}

This global scope ensures the duration of the path is always available on any instance of the Path model. Without the magic of the hasManyThrough using the pivot table, we'd have to iterate over each course and episodes manually (or using a Collection) to sum up the total duration.

Much faster, and much cleaner.

Of course, make sure you're still eager loading in any distant relationship data when listing through any data.

My use case is pretty simple, but if you need more complex deep relationships to be defined, I'd highly recommend the eloquent-has-many-deep package by Jonas Staudenmeir.

Thanks for reading! If you found this article helpful, you might enjoy our practical screencasts too.
Author
Alex Garrett-Smith
Share :

Comments

No comments, yet. Be the first to leave a comment.