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 country

When 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

Example Β· php
<?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.

Example Β· php
// 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.

Example Β· php
$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.

Example Β· php
$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

  • Be the first to comment on this lesson.