Laravel 12 Has Many Through Relationship

Hey there! If you're diving into Laravel 12 and want to master the "has many through" relationship, you’ve come to the right place. In this beginner-friendly guide, I’ll walk you through setting up a has many through relationship using Laravel’s Eloquent ORM.

We’ll use a clear example: a Country that has many Posts through Users. By the end, you’ll know how to define models, migrations, and relationships to access related data across tables.

Laravel 12 Has Many Through Relationship

What is a Has Many Through Relationship?

A has many through relationship allows you to access distant related records through an intermediate model. For example:

  • A Country has many Users.
  • Each User has many Posts.
  • Through Users, a Country has many Posts.

This is useful when you want to retrieve all posts from a country without directly linking the countries and posts tables.

Prerequisites

Before we begin, ensure you have:

  • A Laravel 12 project set up.
  • A database configured (e.g., MySQL, SQLite).
  • Basic knowledge of Laravel models, migrations, and controllers.

If you don’t have a project, create one with:

laravel new example-app

Step 1: Set Up the Database and Migrations

We’ll create three tables:

  • countries: Stores country data.
  • users: Stores user data with a reference to a country.
  • posts: Stores post data with a reference to a user.

Create the Countries Migration

Run this command to create a migration for the countries table:

php artisan make:migration create_countries_table

Open the generated migration file in database/migrations and update it:

<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    public function up(): void
    {
        Schema::create('countries', function (Blueprint $table) {
            $table->id();
            $table->string('name');
            $table->timestamps();
        });
    }

    public function down(): void
    {
        Schema::dropIfExists('countries');
    }
};

Create the Users Migration

Laravel includes a default users migration. Open the migration in database/migrations and modify it to include a country_id foreign key:

<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    public function up(): void
    {
        Schema::create('users', function (Blueprint $table) {
            $table->id();
            $table->string('name');
            $table->string('email')->unique();
            $table->string('password');
            $table->unsignedBigInteger('country_id');
            $table->timestamps();

            $table->foreign('country_id')->references('id')->on('countries')->onDelete('cascade');
        });
    }

    public function down(): void
    {
        Schema::dropIfExists('users');
    }
};

Create the Posts Migration

Run this command to create a migration for the posts table:

php artisan make:migration create_posts_table

Open the generated migration file and update it:

<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    public function up(): void
    {
        Schema::create('posts', function (Blueprint $table) {
            $table->id();
            $table->unsignedBigInteger('user_id');
            $table->string('title');
            $table->text('content');
            $table->timestamps();

            $table->foreign('user_id')->references('id')->on('users')->onDelete('cascade');
        });
    }

    public function down(): void
    {
        Schema::dropIfExists('posts');
    }
};

Explanation:

  • country_id in users links to countries.
  • user_id in posts links to users.
  • onDelete('cascade') ensures related records are deleted if the parent record is removed.

Run the Migrations

Run the migrations to create the tables:

php artisan migrate

Step 2: Create the Models

Define the Country, User, and Post models and set up their relationships.

Create the Country Model

Run:

php artisan make:model Country

Open app/Models/Country.php and add the hasMany and hasManyThrough relationships:

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\HasManyThrough;

class Country extends Model
{
    protected $fillable = ['name'];

    public function users(): HasMany
    {
        return $this->hasMany(User::class);
    }

    public function posts(): HasManyThrough
    {
        return $this->hasManyThrough(Post::class, User::class);
    }
}

Explanation:

  • hasMany defines that a Country has many Users.
  • hasManyThrough defines that a Country has many Posts through Users.

Update the User Model

Open app/Models/User.php and add the belongsTo and hasMany relationships:

<?php

namespace App\Models;

use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;

class User extends Authenticatable
{
    protected $fillable = ['name', 'email', 'password', 'country_id'];

    public function country(): BelongsTo
    {
        return $this->belongsTo(Country::class);
    }

    public function posts(): HasMany
    {
        return $this->hasMany(Post::class);
    }
}

Explanation:

  • belongsTo indicates a User belongs to a Country.
  • hasMany indicates a User has many Posts.

Create the Post Model

Run:

php artisan make:model Post

Open app/Models/Post.php and add the belongsTo relationship:

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;

class Post extends Model
{
    protected $fillable = ['user_id', 'title', 'content'];

    public function user(): BelongsTo
    {
        return $this->belongsTo(User::class);
    }
}

Explanation: The belongsTo method indicates a Post belongs to a User.

Step 3: Set Up Routes and Controller

Create a controller to test the has many through relationship and a route to access it.

Create a Controller

Run:

php artisan make:controller CountryController

Open app/Http/Controllers/CountryController.php and add:

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use App\Models\Country;
use App\Models\User;
use App\Models\Post;

class CountryController extends Controller
{
    public function index()
    {
        // Create a country
        $country = Country::create(['name' => 'Canada']);

        // Create a user for the country
        $user = User::create([
            'name' => 'Alice Smith',
            'email' => '[email protected]',
            'password' => bcrypt('password'),
            'country_id' => $country->id,
        ]);

        // Create posts for the user
        $user->posts()->createMany([
            [
                'title' => 'First Post',
                'content' => 'Welcome to my blog!',
            ],
            [
                'title' => 'Second Post',
                'content' => 'Exploring Laravel!',
            ],
        ]);

        // Retrieve the country with its posts
        $countryWithPosts = Country::with('posts')->first();

        return response()->json([
            'country' => $countryWithPosts->name,
            'posts' => $countryWithPosts->posts->map(function ($post) {
                return [
                    'title' => $post->title,
                    'content' => $post->content,
                    'author' => $post->user->name,
                ];
            }),
        ]);
    }
}

Explanation:

  • Creates a country, a user, and multiple posts.
  • Uses with('posts') to eager-load the posts through users.
  • Returns a JSON response with the country and its posts, including the author’s name.

Define a Route

Open routes/web.php and add:

<?php

use Illuminate\Support\Facades\Route;
use App\Http\Controllers\CountryController;

Route::get('/test-relationship', [CountryController::class, 'index'])->name('test.relationship');

Step 4: Test the Relationship

Start your Laravel server:

php artisan serve

Visit http://localhost:8000/test-relationship in your browser. You should see a JSON response like:

{
    "country": "Canada",
    "posts": [
        {
            "title": "First Post",
            "content": "Welcome to my blog!",
            "author": "Alice Smith"
        },
        {
            "title": "Second Post",
            "content": "Exploring Laravel!",
            "author": "Alice Smith"
        }
    ]
}

This confirms the has many through relationship is working!

Step 5: Using the Relationship in Your Application

You can access related data in various ways:

Retrieve a Country’s Posts

$country = Country::find(1);
$posts = $country->posts; // Returns a collection of Posts
foreach ($posts as $post) {
    echo $post->title; // Outputs: First Post, Second Post
}

Access the Intermediate User

$post = Post::find(1);
$user = $post->user; // Returns the associated User
$country = $user->country; // Returns the associated Country
echo $country->name; // Outputs: Canada

In a Blade Template

Create a Blade file resources/views/country.blade.php:

<!DOCTYPE html>
<html>
<head>
    <title>Laravel 12 Has Many Through Relationship</title>
    <link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
    <div class="container mt-5">
        <h3>Posts from {{ $country->name }}</h3>
        <h4>Posts:</h4>
        <ul>
            @foreach ($country->posts as $post)
                <li>
                    <strong>{{ $post->title }}</strong>: {{ $post->content }} (by {{ $post->user->name }})
                </li>
            @endforeach
        </ul>
    </div>
</body>
</html>

Update the CountryController to return the view:

public function index()
{
    $country = Country::with('posts')->first();
    return view('country', compact('country'));
}

Conclusion

Implementing a has many through relationship in Laravel 12 is a powerful way to access distant related data with Eloquent ORM. This tutorial showed you how to create migrations, define models, and set up a has many through relationship using a Country, Users, and Posts example.

Whether you’re building a global blog or another complex system, this approach simplifies data retrieval. I hope this guide was clear and helps you add has many through relationships to your Laravel projects.

Frequently Asked Questions (FAQs)

Q1: When should I use a has many through relationship?
A: Use it when you need to access records in a distant table through an intermediate model, like getting all posts in a country through users.

Q2: What’s the difference between hasMany and hasManyThrough?
A: hasMany defines a direct one-to-many relationship (e.g., User has many Posts). hasManyThrough accesses records through an intermediate model (e.g., Country has many Posts through Users).

Q3: Why use onDelete('cascade')?
A: It ensures data integrity by deleting related records (e.g., posts or users) when the parent record (e.g., country or user) is deleted.

Q4: How do I handle cases where a country has no posts?
A: The posts relationship returns an empty collection if no posts exist. Check with @if($country->posts->isEmpty()) in Blade or $country->posts->isEmpty() in PHP.

Q5: Can I add extra conditions to the has many through relationship?
A: Yes! Modify the relationship in the model, e.g.:

public function posts(): HasManyThrough
{
    return $this->hasManyThrough(Post::class, User::class)->where('posts.created_at', '>=', now()->subMonth());
}

This filters posts from the last month.


You might also like:

techsolutionstuff

Techsolutionstuff | The Complete Guide

I'm a software engineer and the founder of techsolutionstuff.com. Hailing from India, I craft articles, tutorials, tricks, and tips to aid developers. Explore Laravel, PHP, MySQL, jQuery, Bootstrap, Node.js, Vue.js, and AngularJS in our tech stack.

RECOMMENDED POSTS

FEATURE POSTS