Di bagian sebelumnya kita sudah menguasai Eloquent untuk operasi CRUD pada satu tabel. Tapi aplikasi nyata hampir tidak pernah hanya berurusan dengan satu tabel — data selalu saling berkaitan. Post ditulis oleh User, Post memiliki banyak Comment, Post termasuk dalam Category, dan Post bisa memiliki banyak Tag. Inilah inti dari "relasional" dalam database relasional, dan Eloquent menyediakan cara yang sangat elegan untuk mengekspresikan semua hubungan ini.

💡 Ini adalah bagian ketujuh dari seri "Belajar Laravel dari Nol". Bagian ini membangun langsung di atas Bagian 5 (Migration) dan Bagian 6 (Eloquent ORM). Pastikan kamu sudah memahami konsep foreign key di migration dan cara dasar Eloquent sebelum melanjutkan.


Jenis-Jenis Relasi di Eloquent

Eloquent mendukung semua jenis relasi yang umum ditemukan dalam database relasional, plus beberapa relasi tingkat lanjut yang sangat berguna untuk kasus-kasus spesifik. Sebelum masuk ke kode, penting untuk memahami kapan menggunakan jenis relasi yang mana — karena memilih relasi yang salah akan membuat query menjadi rumit dan tidak efisien.

Bash
─── PETA RELASI ELOQUENT ─────────────────────────────────

ONE-TO-ONE (Satu ke Satu)
hasOne / belongsTo
Contoh: User hasOne Profile, Post hasOne Seo

ONE-TO-MANY (Satu ke Banyak) ← paling umum
hasMany / belongsTo
Contoh: User hasMany Post, Post hasMany Comment
        Category hasMany Post

MANY-TO-MANY (Banyak ke Banyak)
belongsToMany / belongsToMany
Contoh: Post belongsToMany Tag, User belongsToMany Role

─── RELASI TINGKAT LANJUT ────────────────────────────────

HAS ONE THROUGH
Contoh: Country hasOneThrough Capital through City

HAS MANY THROUGH
Contoh: Country hasManyThrough Post through User
        (Semua post dari semua user di sebuah negara)

POLYMORPHIC ONE-TO-ONE
morphOne / morphTo
Contoh: Image morphTo (bisa milik Post ATAU User)

POLYMORPHIC ONE-TO-MANY
morphMany / morphTo
Contoh: Comment morphTo (bisa milik Post ATAU Video)

POLYMORPHIC MANY-TO-MANY
morphToMany / morphedByMany
Contoh: Tag morphToMany (bisa menandai Post ATAU Video)


One-to-One: hasOne dan belongsTo

Relasi One-to-One menghubungkan satu record di tabel A dengan tepat satu record di tabel B. Contoh paling umum adalah User dan Profile — satu pengguna memiliki tepat satu profil, dan satu profil hanya dimiliki satu pengguna. Di database, foreign key biasanya ada di tabel yang "dimiliki" (profiles.user_id).

PHP
<?php
// ── MIGRATION ─────────────────────────────────────────

// database/migrations/create_profiles_table.php
Schema::create('profiles', function (Blueprint $table) {
    $table->id();
    $table->foreignId('user_id')->unique()->constrained()->cascadeOnDelete();
    // unique() memastikan satu user hanya punya satu profile
    $table->string('bio')->nullable();
    $table->string('avatar')->nullable();
    $table->string('website')->nullable();
    $table->string('location')->nullable();
    $table->timestamps();
});
PHP
<?php
// ── MODEL ─────────────────────────────────────────────

// app/Models/User.php
class User extends Authenticatable
{
    // User MEMILIKI satu Profile
    // "hasOne" karena foreign key ada di tabel profiles
    public function profile(): HasOne
    {
        return $this->hasOne(Profile::class);
        // Laravel otomatis asumsikan foreign key: profiles.user_id
        // Bisa dikustomisasi: $this->hasOne(Profile::class, 'foreign_key', 'local_key')
    }
}

// app/Models/Profile.php
class Profile extends Model
{
    // Profile MILIK satu User
    // "belongsTo" karena foreign key ada di tabel INI (profiles.user_id)
    public function user(): BelongsTo
    {
        return $this->belongsTo(User::class);
        // Laravel otomatis asumsikan foreign key: user_id
    }
}
PHP
<?php
// ── PENGGUNAAN ────────────────────────────────────────

// Akses profile dari user
$user    = User::find(1);
$profile = $user->profile; // Instance Profile atau null

echo $user->profile->bio;
echo $user->profile->avatar;

// Akses user dari profile
$profile = Profile::find(1);
$user    = $profile->user;
echo $profile->user->name;
echo $profile->user->email;

// Buat profile untuk user
$user->profile()->create([
    'bio'      => 'PHP Developer dari Jakarta',
    'website'  => 'https://devpath.id',
    'location' => 'Jakarta, Indonesia',
]);

// Update profile
$user->profile()->update(['bio' => 'Senior Laravel Developer']);

// Atau menggunakan updateOrCreate (jika profile belum tentu ada)
$user->profile()->updateOrCreate(
    ['user_id' => $user->id],
    ['bio' => 'PHP Developer', 'location' => 'Jakarta']
);

// Eager loading (penting untuk hindari N+1)
$users = User::with('profile')->get();
foreach ($users as $user) {
    echo $user->profile?->bio; // Tidak ada query tambahan!
}


One-to-Many: hasMany dan belongsTo

Relasi One-to-Many adalah yang paling sering digunakan dalam aplikasi web — satu record di tabel A memiliki banyak record di tabel B. Contoh: satu User memiliki banyak Post, satu Post memiliki banyak Comment, satu Category memiliki banyak Post. Foreign key selalu ada di sisi "banyak" (tabel anak).

PHP
<?php
// ── MODEL ─────────────────────────────────────────────

// app/Models/User.php
class User extends Authenticatable
{
    // User MEMILIKI BANYAK Post
    public function posts(): HasMany
    {
        return $this->hasMany(Post::class);
        // Foreign key: posts.user_id
    }

    // Bisa tambahkan constraint di definisi relasi
    public function publishedPosts(): HasMany
    {
        return $this->hasMany(Post::class)
            ->where('status', 'published')
            ->latest('published_at');
    }

    public function latestPost(): HasOne
    {
        // HasOne dari HasMany — ambil satu record terbaru
        return $this->hasOne(Post::class)->latestOfMany();
    }
}

// app/Models/Post.php
class Post extends Model
{
    // Post MILIK satu User
    public function user(): BelongsTo
    {
        return $this->belongsTo(User::class);
    }

    // Post MILIK satu Category
    public function category(): BelongsTo
    {
        return $this->belongsTo(Category::class);
    }

    // Post MEMILIKI BANYAK Comment
    public function comments(): HasMany
    {
        return $this->hasMany(Comment::class);
    }

    // Hanya komentar yang sudah diapprove
    public function approvedComments(): HasMany
    {
        return $this->hasMany(Comment::class)
            ->where('is_approved', true)
            ->latest();
    }
}

// app/Models/Category.php
class Category extends Model
{
    // Category MEMILIKI BANYAK Post
    public function posts(): HasMany
    {
        return $this->hasMany(Post::class);
    }

    // Hanya post yang published
    public function publishedPosts(): HasMany
    {
        return $this->hasMany(Post::class)
            ->where('status', 'published');
    }
}
PHP
<?php
// ── PENGGUNAAN hasMany ────────────────────────────────

$user = User::find(1);

// Ambil semua post milik user
$posts = $user->posts;                    // Collection of Post
$posts = $user->posts()->get();           // Sama, tapi bisa di-chain query
$posts = $user->posts()->latest()->get(); // Dengan tambahan query

// Count tanpa load semua data
$count = $user->posts()->count();
$count = $user->posts_count; // Jika pakai withCount()

// Cek apakah user punya post
$hasPosts = $user->posts()->exists();

// Buat post baru otomatis dengan user_id yang benar
$post = $user->posts()->create([
    'title'   => 'Post Baru',
    'content' => 'Konten post...',
    'status'  => 'draft',
    'slug'    => 'post-baru',
]);
// user_id otomatis diisi dengan $user->id

// Membuat beberapa sekaligus
$user->posts()->createMany([
    ['title' => 'Post 1', 'content' => '...', 'slug' => 'post-1'],
    ['title' => 'Post 2', 'content' => '...', 'slug' => 'post-2'],
]);

// ── PENGGUNAAN belongsTo ──────────────────────────────
$post = Post::find(1);

// Akses relasi
echo $post->user->name;         // Nama penulis
echo $post->category->name;     // Nama kategori

// Ubah category post
$post->category()->associate($newCategory); // Set category_id
$post->save();

$post->category()->dissociate(); // Set category_id ke null
$post->save();


Many-to-Many: belongsToMany

Relasi Many-to-Many digunakan ketika satu record di tabel A bisa berelasi dengan banyak record di tabel B, dan sebaliknya. Contoh: Post bisa memiliki banyak Tag, dan satu Tag bisa digunakan di banyak Post. Relasi ini membutuhkan tabel pivot (tabel penghubung) di database yang menyimpan pasangan ID dari kedua tabel.

PHP
<?php
// ── MIGRATION TABEL PIVOT ─────────────────────────────

Schema::create('post_tag', function (Blueprint $table) {
    // Konvensi nama: singular kedua model, urutan alfabetis
    // post + tag → post_tag (bukan tag_post)
    $table->foreignId('post_id')->constrained()->cascadeOnDelete();
    $table->foreignId('tag_id')->constrained()->cascadeOnDelete();
    $table->primary(['post_id', 'tag_id']); // Composite PK
    // Bisa tambah kolom ekstra di pivot:
    // $table->integer('order')->default(0);
    // $table->timestamps();
});
PHP
<?php
// ── MODEL ─────────────────────────────────────────────

// app/Models/Post.php
class Post extends Model
{
    // Post BISA MEMILIKI BANYAK Tag (dan sebaliknya)
    public function tags(): BelongsToMany
    {
        return $this->belongsToMany(Tag::class);
        // Laravel otomatis asumsikan:
        // - Tabel pivot: post_tag
        // - FK di pivot: post_id, tag_id

        // Kustomisasi jika perlu:
        // return $this->belongsToMany(Tag::class, 'post_tag', 'post_id', 'tag_id');
    }

    // Jika tabel pivot punya timestamps
    public function tagsWithTimestamps(): BelongsToMany
    {
        return $this->belongsToMany(Tag::class)->withTimestamps();
    }

    // Jika tabel pivot punya kolom ekstra
    public function tagsWithOrder(): BelongsToMany
    {
        return $this->belongsToMany(Tag::class)
            ->withPivot('order')        // Akses kolom pivot
            ->orderByPivot('order');    // Sort berdasarkan kolom pivot
    }
}

// app/Models/Tag.php
class Tag extends Model
{
    // Tag juga bisa punya banyak Post
    public function posts(): BelongsToMany
    {
        return $this->belongsToMany(Post::class);
    }
}

// app/Models/User.php — contoh lain Many-to-Many
class User extends Model
{
    // User bisa punya banyak Role
    public function roles(): BelongsToMany
    {
        return $this->belongsToMany(Role::class)
            ->withTimestamps()
            ->withPivot('assigned_by', 'expires_at');
    }
}
PHP
<?php
// ── PENGGUNAAN Many-to-Many ───────────────────────────

$post = Post::find(1);

// Ambil semua tag post ini
$tags = $post->tags;                    // Collection of Tag
$tags = $post->tags()->get();

// ── ATTACH: Tambahkan relasi ke pivot ─────────────────
$post->tags()->attach(1);               // Tambah tag ID 1
$post->tags()->attach([1, 2, 3]);       // Tambah beberapa tag
$post->tags()->attach(1, ['order' => 1]); // Dengan data pivot ekstra

// ── DETACH: Hapus relasi dari pivot ───────────────────
$post->tags()->detach(1);               // Hapus tag ID 1
$post->tags()->detach([1, 2]);          // Hapus beberapa
$post->tags()->detach();                // Hapus SEMUA tag dari post ini

// ── SYNC: Set relasi persis seperti yang diberikan ────
// Sangat berguna saat update form checkbox/multiselect
$post->tags()->sync([1, 2, 3]);
// → Tag yang tidak ada di array akan di-detach
// → Tag yang sudah ada tidak berubah (tidak duplicate)
// → Tag baru di-attach

// Sync tanpa detach yang tidak disebutkan
$post->tags()->syncWithoutDetaching([4, 5]);

// Sync dengan data pivot
$post->tags()->sync([
    1 => ['order' => 1],
    2 => ['order' => 2],
    3 => ['order' => 3],
]);

// ── TOGGLE: Tambah jika belum ada, hapus jika sudah ada ─
$post->tags()->toggle([1, 2, 3]);

// ── CEK RELASI ────────────────────────────────────────
$post->tags()->where('slug', 'laravel')->exists(); // bool

// ── AKSES DATA PIVOT ──────────────────────────────────
foreach ($post->tags as $tag) {
    echo $tag->name;
    echo $tag->pivot->order;       // Kolom pivot
    echo $tag->pivot->created_at;  // Jika withTimestamps()
}


Has Many Through: Relasi Melintasi Tabel

Has Many Through memungkinkan kamu mengakses relasi yang melewati model perantara. Contoh klasik: Country memiliki banyak User, dan setiap User memiliki banyak Post. Dengan hasManyThrough, kamu bisa langsung mengakses semua Post dari sebuah Country — melewati tabel User sebagai perantara — tanpa harus query secara manual.

PHP
<?php

// app/Models/Country.php
class Country extends Model
{
    // Country memiliki banyak User
    public function users(): HasMany
    {
        return $this->hasMany(User::class);
    }

    // Country memiliki banyak Post MELALUI User
    public function posts(): HasManyThrough
    {
        return $this->hasManyThrough(
            Post::class,    // Model tujuan akhir
            User::class,    // Model perantara
            'country_id',   // FK di tabel users yang mengarah ke countries
            'user_id',      // FK di tabel posts yang mengarah ke users
            'id',           // PK di tabel countries
            'id'            // PK di tabel users
        );

        // Versi singkat jika mengikuti konvensi:
        // return $this->hasManyThrough(Post::class, User::class);
    }
}

// ── PENGGUNAAN ────────────────────────────────────────
$country = Country::find(1);

// Semua post dari semua user di negara ini
$posts = $country->posts()->published()->latest()->get();

// Dengan eager loading
$countries = Country::with('posts')->get();


Polymorphic Relations: Relasi yang Fleksibel

Polymorphic relation memungkinkan satu model berelasi dengan beberapa model berbeda menggunakan satu definisi relasi. Contoh paling umum: Comment yang bisa dimiliki oleh Post ATAU Video — tanpa harus membuat tabel comment terpisah untuk setiap jenis konten. Di database, kolom morphable_type menyimpan nama Model pemilik dan morphable_id menyimpan ID-nya.

PHP
<?php
// ── MIGRATION UNTUK POLYMORPHIC ───────────────────────

Schema::create('comments', function (Blueprint $table) {
    $table->id();
    $table->foreignId('user_id')->constrained()->cascadeOnDelete();
    $table->text('body');
    $table->boolean('is_approved')->default(false);

    // Kolom polymorphic — morphs() membuat dua kolom:
    // commentable_type (VARCHAR) dan commentable_id (BIGINT UNSIGNED)
    $table->morphs('commentable');
    // Hasilnya: commentable_type = 'App\Models\Post' atau 'App\Models\Video'
    //           commentable_id   = ID dari record yang bersangkutan

    $table->timestamps();
    $table->index(['commentable_type', 'commentable_id']);
});

// ── MODEL ─────────────────────────────────────────────

// app/Models/Comment.php
class Comment extends Model
{
    // Comment MILIK model apapun (polymorphic)
    public function commentable(): MorphTo
    {
        return $this->morphTo();
        // Otomatis baca commentable_type dan commentable_id
    }

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

// app/Models/Post.php
class Post extends Model
{
    // Post MEMILIKI BANYAK Comment (polymorphic)
    public function comments(): MorphMany
    {
        return $this->morphMany(Comment::class, 'commentable');
    }
}

// app/Models/Video.php
class Video extends Model
{
    // Video juga MEMILIKI BANYAK Comment (polymorphic)
    public function comments(): MorphMany
    {
        return $this->morphMany(Comment::class, 'commentable');
    }
}
PHP
<?php
// ── PENGGUNAAN POLYMORPHIC ────────────────────────────

// Tambah komentar ke Post
$post = Post::find(1);
$post->comments()->create([
    'user_id' => auth()->id(),
    'body'    => 'Artikel yang sangat bermanfaat!',
]);

// Tambah komentar ke Video — cara yang sama persis!
$video = Video::find(1);
$video->comments()->create([
    'user_id' => auth()->id(),
    'body'    => 'Video yang bagus!',
]);

// Ambil semua komentar milik post
$comments = $post->comments()->approved()->latest()->get();

// Dari Comment, akses pemiliknya (bisa Post atau Video)
$comment    = Comment::find(1);
$commentable = $comment->commentable; // Instance Post atau Video

echo get_class($commentable); // "App\Models\Post" atau "App\Models\Video"
echo $commentable->title;     // Judul post atau video

// Eager loading polymorphic
$comments = Comment::with('commentable', 'user')->latest()->get();

// ── POLYMORPHIC MANY-TO-MANY ──────────────────────────
// Tag yang bisa menandai Post ATAU Video

// Migration tabel pivot polymorphic
Schema::create('taggables', function (Blueprint $table) {
    $table->foreignId('tag_id')->constrained()->cascadeOnDelete();
    $table->morphs('taggable'); // taggable_type + taggable_id
    $table->primary(['tag_id', 'taggable_type', 'taggable_id']);
});

// app/Models/Tag.php
class Tag extends Model
{
    public function posts(): MorphedByMany
    {
        return $this->morphedByMany(Post::class, 'taggable');
    }

    public function videos(): MorphedByMany
    {
        return $this->morphedByMany(Video::class, 'taggable');
    }
}

// app/Models/Post.php
class Post extends Model
{
    public function tags(): MorphToMany
    {
        return $this->morphToMany(Tag::class, 'taggable');
    }
}


Eager Loading: Mencegah N+1 Problem

N+1 problem adalah salah satu masalah performa paling umum dalam aplikasi yang menggunakan ORM. Ini terjadi ketika kamu menjalankan 1 query untuk mengambil daftar record, lalu N query tambahan (satu per record) untuk mengambil data relasinya. Untuk 100 post, ini berarti 101 query — sangat tidak efisien. Eager loading adalah solusinya.

PHP
<?php

// ── MASALAH: N+1 Problem ──────────────────────────────
$posts = Post::all(); // Query 1: ambil semua post

foreach ($posts as $post) {
    echo $post->user->name;     // Query 2, 3, 4... (1 query per post!)
    echo $post->category->name; // Query N+1, N+2... (lagi!)
}
// Untuk 100 post: 1 + 100 + 100 = 201 QUERY! 🚨

// ── SOLUSI: Eager Loading dengan with() ───────────────
$posts = Post::with(['user', 'category'])->get();
// Query 1: SELECT * FROM posts
// Query 2: SELECT * FROM users WHERE id IN (1, 2, 3, ...)
// Query 3: SELECT * FROM categories WHERE id IN (1, 2, ...)
// Total: 3 QUERY saja! ✅

foreach ($posts as $post) {
    echo $post->user->name;     // Tidak ada query tambahan!
    echo $post->category->name; // Tidak ada query tambahan!
}

// ── EAGER LOADING BERSARANG (Nested) ─────────────────
$posts = Post::with([
    'user',                          // Load user
    'user.profile',                  // Load profile dari user
    'category',                      // Load category
    'tags',                          // Load semua tag
    'comments',                      // Load semua comment
    'comments.user',                 // Load user dari setiap comment
])->get();

// ── EAGER LOADING DENGAN CONSTRAINT ───────────────────
$posts = Post::with([
    'comments' => function ($query) {
        $query->where('is_approved', true)
              ->latest()
              ->limit(3);
    },
    'tags:id,name,slug',  // Hanya kolom tertentu
    'user:id,name,avatar' // Hanya kolom tertentu (selalu sertakan id + FK!)
])->get();

// ── withCount: Hitung relasi tanpa load datanya ────────
$posts = Post::withCount([
    'comments',                              // → $post->comments_count
    'comments as approved_comments_count' => // → $post->approved_comments_count
        fn($q) => $q->where('is_approved', true),
    'tags',                                  // → $post->tags_count
])->get();

foreach ($posts as $post) {
    echo $post->comments_count;          // Jumlah total komentar
    echo $post->approved_comments_count; // Jumlah komentar approved
    echo $post->tags_count;              // Jumlah tag
}

// ── lazy loading vs eager loading ─────────────────────
// Deteksi N+1 di development: install package Debugbar
// composer require barryvdh/laravel-debugbar --dev

// Atau aktifkan strict mode (throw exception jika lazy load):
// Model::preventLazyLoading(app()->isLocal());

⚠️ N+1 problem adalah salah satu penyebab paling umum aplikasi Laravel menjadi lambat di production. Selalu gunakan with() saat kamu tahu akan mengakses relasi di dalam loop. Untuk mendeteksi N+1 problem secara otomatis selama development, tambahkan Model::preventLazyLoading(true) di AppServiceProvider — Laravel akan melempar exception setiap kali ada lazy loading yang seharusnya di-eager load.


whereHas dan doesntHave: Filter Berdasarkan Relasi

Sering kali kamu perlu memfilter record berdasarkan kondisi di tabel relasinya — misalnya "tampilkan hanya post yang memiliki setidaknya satu komentar" atau "tampilkan user yang belum pernah membuat post". Eloquent menyediakan whereHas() dan doesntHave() untuk kebutuhan ini.

PHP
<?php

// ── whereHas: Hanya record yang MEMILIKI relasi ────────
// Post yang punya minimal 1 komentar
$posts = Post::whereHas('comments')->get();

// Post yang punya komentar approved
$posts = Post::whereHas('comments', function ($query) {
    $query->where('is_approved', true);
})->get();

// Post yang punya tag 'laravel'
$posts = Post::whereHas('tags', function ($query) {
    $query->where('slug', 'laravel');
})->get();

// Post yang punya minimal 5 komentar
$posts = Post::has('comments', '>=', 5)->get();

// ── doesntHave / whereDoesntHave ──────────────────────
// Post yang TIDAK punya komentar
$posts = Post::doesntHave('comments')->get();

// User yang TIDAK punya post published
$users = User::whereDoesntHave('posts', function ($query) {
    $query->where('status', 'published');
})->get();

// ── withWhereHas: Load relasi + filter sekaligus ───────
// Hanya load tag 'laravel', DAN hanya post yang punya tag tersebut
$posts = Post::withWhereHas('tags', function ($query) {
    $query->where('slug', 'laravel');
})->get();

Praktik: Model Lengkap untuk Aplikasi Blog

Mari satukan semua jenis relasi yang sudah dipelajari dalam contoh nyata — mendefinisikan semua relasi yang dibutuhkan aplikasi blog lengkap dengan cara penggunaan yang benar di Controller dan View.

PHP
<?php
// app/Models/User.php — Semua relasi user

namespace App\Models;

use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Database\Eloquent\Relations\{HasOne, HasMany};

class User extends Authenticatable
{
    // One-to-One: User punya satu Profile
    public function profile(): HasOne
    {
        return $this->hasOne(Profile::class);
    }

    // One-to-Many: User punya banyak Post
    public function posts(): HasMany
    {
        return $this->hasMany(Post::class);
    }

    // One-to-Many dengan constraint
    public function publishedPosts(): HasMany
    {
        return $this->hasMany(Post::class)
            ->where('status', 'published')
            ->latest('published_at');
    }

    // One-to-Many: User punya banyak Comment
    public function comments(): HasMany
    {
        return $this->hasMany(Comment::class);
    }

    // Many-to-Many: User punya banyak Role
    public function roles(): BelongsToMany
    {
        return $this->belongsToMany(Role::class)
            ->withTimestamps()
            ->withPivot('assigned_by');
    }

    // Helper method untuk cek role
    public function hasRole(string $role): bool
    {
        return $this->roles()->where('slug', $role)->exists();
    }
}
PHP
<?php
// app/Models/Post.php — Semua relasi post

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Database\Eloquent\Relations\{
    BelongsTo, HasMany, MorphMany, BelongsToMany
};

class Post extends Model
{
    use SoftDeletes;

    // Post milik satu User (penulis)
    public function user(): BelongsTo
    {
        return $this->belongsTo(User::class);
    }

    // Post milik satu Category
    public function category(): BelongsTo
    {
        return $this->belongsTo(Category::class);
    }

    // Post punya banyak Tag (many-to-many)
    public function tags(): BelongsToMany
    {
        return $this->belongsToMany(Tag::class)
            ->withTimestamps();
    }

    // Post punya banyak Comment (polymorphic)
    public function comments(): MorphMany
    {
        return $this->morphMany(Comment::class, 'commentable');
    }

    // Hanya approved comment
    public function approvedComments(): MorphMany
    {
        return $this->morphMany(Comment::class, 'commentable')
            ->where('is_approved', true)
            ->latest();
    }

    // Post punya banyak Like (polymorphic)
    public function likes(): MorphMany
    {
        return $this->morphMany(Like::class, 'likeable');
    }
}
PHP
<?php
// Controller — penggunaan relasi yang optimal

namespace App\Http\Controllers;

use App\Models\Post;
use Illuminate\View\View;

class PostController extends Controller
{
    public function index(): View
    {
        $posts = Post::published()
            ->with([
                'user:id,name,avatar',
                'category:id,name,slug,color',
                'tags:id,name,slug',
            ])
            ->withCount(['comments', 'likes'])
            ->latest('published_at')
            ->paginate(12);

        return view('posts.index', compact('posts'));
    }

    public function show(Post $post): View
    {
        $post->load([
            'user.profile',
            'category',
            'tags',
            'approvedComments.user:id,name,avatar',
        ]);

        $post->loadCount(['comments', 'likes']);
        $post->increment('views');

        $relatedPosts = Post::published()
            ->whereHas('tags', fn($q) =>
                $q->whereIn('id', $post->tags->pluck('id'))
            )
            ->where('id', '!=', $post->id)
            ->with('user:id,name', 'category:id,name,slug')
            ->take(3)
            ->get();

        return view('posts.show', compact('post', 'relatedPosts'));
    }
}

💡 Perhatikan pola load() dan loadCount() yang digunakan di method show() — keduanya adalah "lazy eager loading" yang berguna saat kamu sudah memiliki instance Model (misalnya dari Route Model Binding) tapi perlu memuat relasinya setelah itu. Ini berbeda dari with() yang digunakan saat query, tapi hasilnya sama-sama efisien karena hanya menjalankan query tambahan sekali untuk semua relasi yang disebutkan.


Langkah Selanjutnya

Luar biasa — kamu sudah menguasai semua jenis relasi Eloquent yang paling penting! Mulai dari One-to-One, One-to-Many, Many-to-Many, Has Many Through, hingga Polymorphic Relations yang fleksibel. Kamu juga sudah memahami cara mencegah N+1 problem dengan eager loading dan cara memfilter berdasarkan relasi dengan whereHas. Di bagian kedelapan seri ini, kita akan membahas Form, Validasi, dan Request Handling secara mendalam — termasuk Form Request class, custom validation rules, dan cara mengelola form yang kompleks dengan file upload.

🚀 Sebelum lanjut ke Bagian 8, pastikan kamu sudah berlatih: (1) mendefinisikan semua jenis relasi di Model dan mencobanya di Tinker, (2) menggunakan with() untuk eager loading di Controller dan memverifikasi jumlah query di Debugbar, (3) menggunakan attach(), detach(), dan sync() untuk relasi many-to-many, dan (4) mencoba polymorphic relation untuk Comment yang bisa dimiliki oleh Post dan model lain. Relasi Eloquent adalah konsep yang paling cepat dikuasai melalui praktik langsung — semakin banyak kamu bereksperimen, semakin intuitif ia terasa.