A has-many-through relationship in Laravel 13 lets a model reach distant records through an intermediate model. If a Country has many Users and a User has many Posts, Country::posts() can return all posts for the country with hasManyThrough(Post::class, User::class), with no manual joins.
What do you need before you start?
- Laravel 13 on PHP 8.3 or newer
- Three tables:
countries,users(withcountry_id) andposts(withuser_id)
What does the schema look like?
countries
id, name
users
id, country_id, name
posts
id, user_id, title// users migration (excerpt)
$table->foreignId('country_id')->constrained();
// posts migration (excerpt)
$table->foreignId('user_id')->constrained();How do you define the relationship?
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasManyThrough;
class Country extends Model
{
public function users()
{
return $this->hasMany(User::class);
}
public function posts(): HasManyThrough
{
return $this->hasManyThrough(Post::class, User::class);
}
}The first argument is the final model you want. The second is the intermediate model. Laravel guesses the keys from the names: users.country_id and posts.user_id.
How do you use it?
$country = Country::find(1);
foreach ($country->posts as $post) {
echo $post->title;
}
// add constraints like any relationship
$recent = $country->posts()->where('posts.created_at', '>=', now()->subWeek())->get();Qualify column names with the table (for example posts.created_at) because the generated query joins two tables and some columns exist in both.
How do you customize the keys?
Pass four more arguments when your column names do not follow convention.
return $this->hasManyThrough(
Post::class,
User::class,
'country_id', // foreign key on users
'author_id', // foreign key on posts
'id', // local key on countries
'id' // local key on users
);What about has-one-through?
Use hasOneThrough() when you want one related record, such as the latest owner of a car through a mechanic. It takes the same arguments.
public function carOwner(): HasOneThrough
{
return $this->hasOneThrough(Owner::class, Car::class);
}How do you avoid N+1 queries?
$countries = Country::with('posts')->get();
$countries = Country::withCount('posts')->get();
foreach ($countries as $country) {
echo $country->name.': '.$country->posts_count;
}How do you verify it works?
public function test_country_has_posts_through_users(): void
{
$country = Country::factory()->create();
$user = User::factory()->create(['country_id' => $country->id]);
Post::factory()->count(3)->create(['user_id' => $user->id]);
$this->assertCount(3, $country->posts);
}What are the common mistakes?
- Wrong argument order: the final model comes first, the intermediate model second
- Ambiguous column errors because the column name was not table-qualified
- Trying to attach or save through the relationship. It is read-oriented. Create the posts from the user instead
- Soft-deleted intermediate rows being ignored or included unexpectedly
FAQ
Can I go through more than two hops?
Not with one built-in relationship. Chain relationships, or write a query with joins.
Does hasManyThrough support eager loading?
Yes. Use with('posts') like any relationship.
Is it faster than looping users?
It runs one joined query instead of one query per user, so it is usually better, but check your indexes on the foreign keys.



