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.
path_id
, references the path_id
column on the course_path
pivot table.course_id
, references the course_id
on the episodes
table.id
, references the id
on the paths
table.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.