hasManyThrough & the -Through family
Reach across an intermediate table to grab distant records in one clean relationship.
Sometimes the data you want lives two tables away. A Country has many Users, and each user writes many Posts. If you want all posts from a country, you could loop users and collect their posts — but that is a lot of queries and a lot of code. hasManyThrough gives you that distant collection directly.
How to read it
Think of it as "I have many final models, through an intermediate one." Laravel walks the chain for you using foreign keys it can usually infer by convention.
// On the Country model
public function posts()
{
// Country -> users.country_id -> posts.user_id
return $this->hasManyThrough(Post::class, User::class);
}
// $country->posts // every post written by users in that countryWhen to prefer it
- The middle model is just a bridge you do not need in the result.
- You want a single relationship you can eager-load, count, and constrain.
Laravel 12 also ships hasOneThrough for the single-record version, and you can express the same chains fluently with through()->has() when the key names get unusual.
Example
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasManyThrough;
class Country extends Model
{
/**
* All posts authored by users who belong to this country.
* Chain: countries.id <- users.country_id ... users.id <- posts.user_id
*/
public function posts(): HasManyThrough
{
return $this->hasManyThrough(
related: Post::class, // the model we ultimately want
through: User::class, // the bridge model
firstKey: 'country_id', // FK on users pointing back to countries
secondKey:'user_id', // FK on posts pointing back to users
localKey: 'id', // PK on countries
secondLocalKey: 'id', // PK on users
);
}
}
// --- Usage in a controller / service ---
$country = Country::query()
->withCount('posts') // COUNT via the through relation
->with(['posts' => fn ($q) => $q->latest()->limit(5)])
->findOrFail($id);
// One extra query for the posts, not one-per-user:
foreach ($country->posts as $post) {
// ...
}When to use it
- A country model accesses all posts written by any of its users through hasManyThrough, without writing a manual JOIN across three tables.
- A university model retrieves all exam submissions from students enrolled in any of its departments using a hasManyThrough chain.
- A company HR system reaches all timesheets submitted by employees assigned to any of the company's departments through a two-level relationship.
More examples
Define hasManyThrough
hasManyThrough reaches Post through User, letting you call $country->posts without manually joining three tables.
// Country -> Users -> Posts
class Country extends Model
{
public function posts(): HasManyThrough
{
return $this->hasManyThrough(
Post::class, // final model
User::class, // intermediate model
'country_id', // FK on users table
'user_id', // FK on posts table
'id', // local key on countries
'id' // local key on users
);
}
}Query through the relationship
Uses the relationship as a dynamic property for all posts or as a method to apply additional query constraints.
$country = Country::find(1);
// All posts by users in this country
$posts = $country->posts;
// Filtered posts
$published = $country->posts()
->where('status', 'published')
->latest()
->paginate(10);Eager load hasManyThrough
Eager loading hasManyThrough with with() resolves the three-table relationship in exactly two queries regardless of how many countries exist.
$countries = Country::with('posts')->get();
foreach ($countries as $country) {
echo $country->name . ': ' . $country->posts->count() . ' posts';
}
// Only 2 SQL queries for all countries and all their posts
Discussion