Sepanjang section ini kita selalu mendefinisikan relasi berpasangan: hasMany() di satu sisi, belongsTo() di sisi lain; morphMany() di induk, morphTo() di anak. Bab ini mengangkat sisi balik itu ke panggung utama — bukan sekadar pelengkap, melainkan pola tersendiri yang membuat relasi bisa ditelusuri dua arah. Dari sebuah nomor telepon kita bisa menemukan pemiliknya; dari sebuah comment kita bisa kembali ke post-nya atau bahkan video-nya; dari sebuah post kita bisa melompat dua tingkat ke alamat penulisnya. Kita tutup dengan with(), cara paling sering dipakai untuk memuat relasi balik secara efisien.

belongsTo: Jalan Kembali dari Anak #

Setiap relasi yang meletakkan foreign key di tabel anak — hasOne maupun hasMany — dibalik dengan belongsTo di model anak. Hasilnya selalu satu model, bukan collection, karena foreign key hanya menyimpan satu nilai:

php
$phone = Phone::first();
echo 'nomor ', $phone->nomor, ' milik ', $phone->user->name, ' (', $phone->user->email, ")\n";
echo 'class hasil relasi: ', get_class($phone->user), "\n";

$post2 = Post::where('judul', 'Memahami Migration di Laravel')->first();
echo $post2->judul, ' ditulis ', $post2->user->name, ' (', $post2->user->email, ")\n";
echo 'class hasil relasi: ', get_class($post2->user), "\n";

Output yang Dihasilkan:

text
nomor 0812-3456-7890 milik Budi ([email protected])
class hasil relasi: App\Models\User
Memahami Migration di Laravel ditulis Siti ([email protected])
class hasil relasi: App\Models\User

Baris class hasil relasi muncul dua kali dengan isi yang sama karena kedua relasi mengembalikan model User — $phone->user adalah inverse dari hasOne, $post2->user adalah inverse dari hasMany — dua relasi maju yang berbeda, satu bentuk jalan kembali. Keduanya menjalankan query select * from "users" where "users"."id" = ? limit 1 dengan nilai foreign key dari model yang kita pegang.

morphTo: Jalan Kembali dari Anak Polymorphic #

Untuk anak polymorphic, jalan baliknya adalah morphTo() — dan inilah satu-satunya inverse yang tidak perlu tahu jenis induknya di awal:

php
foreach (Comment::orderBy('id')->get() as $c) {
    echo $c->isi, ' berada di ', $c->commentable::class, ': ', $c->commentable->judul, "\n";
}

Output yang Dihasilkan:

text
Materinya sangat membantu! berada di App\Models\Post: Tips Produktif Belajar Laravel
Terima kasih, bang. berada di App\Models\Post: Tips Produktif Belajar Laravel
Videonya jelas sekali! berada di App\Models\Video: Instalasi Laravel 13 dari Nol

Loop yang sama mengembalikan model Post untuk dua comment pertama dan model Video untuk comment ketiga. Method morphTo() membaca commentable_type dari setiap baris, lalu memuat class yang tercatat di kolom itu. Inverse polymorphic inilah yang membuat chapter Polymorphic Relationships bisa menampilkan induk apa pun tanpa percabangan kode.

Konsistensi Penamaan sebagai Kompas #

Membaca nama relasi seharusnya cukup untuk menebak isinya. Konvensi Eloquent: nama method belongsTo memakai bentuk tunggal — $comment->post, $phone->user, $post->user — sedangkan hasMany dan sejenisnya memakai bentuk jamak — $post->comments, $user->roles. Konsistensi ini bukan sekadar keindahan. Nama relasi menjadi alamat yang bisa dipakai di banyak tempat: with('user'), relasi bersarang seperti comment.user, hingga pengecekan relasi di authorization. Di chapter One to Many , inverse $comment->post membuat kode pembacaan komentar terasa seperti kalimat — dan setiap relasi maju yang kita definisikan sudah selayaknya punya inverse bernama rapi seperti itu.

Menembus Dua Tingkat: hasOneThrough #

Kadang data yang kita cari berjarak dua relasi. Sebuah post ditulis user, dan user memiliki satu address berisi kode pos — untuk menampilkan kode pos penulis sebuah post, kita melewati post lalu user lalu address. Relasi hasOneThrough() memetakan lompatan itu menjadi satu method di model Post:

php
// app/Models/Post.php
public function authorAddress(): HasOneThrough
{
    return $this->hasOneThrough(
        Address::class,   // model tujuan
        User::class,      // model perantara
        'id',             // kolom users yang dicocokkan
        'user_id',        // kolom addresses yang merujuk users
        'user_id',        // kolom posts sebagai titik berangkat
        'id',             // kolom users yang dirujuk addresses
    );
}

Empat nama kolom terakhir diperlukan karena lompatan kita tidak mengikuti konvensi bawaan — relasi berangkat dari posts.user_id, bukan dari primary key yang dirujuk tabel tengah. Mari kita lihat SQL yang dibangunnya dan hasilnya:

php
$post1 = Post::where('judul', 'Tips Produktif Belajar Laravel')->first();
$post2 = Post::where('judul', 'Memahami Migration di Laravel')->first();

echo 'SQL: [', $post1->authorAddress()->toSql(), "]\n";
echo 'kode pos penulis post 1: ', $post1->authorAddress->kode_pos, "\n";
echo 'kode pos penulis post 2: ', $post2->authorAddress->kode_pos, "\n";

Output yang Dihasilkan:

text
SQL: [select * from "addresses" inner join "users" on "users"."id" = "addresses"."user_id" where "users"."id" = ?]
kode pos penulis post 1: 10110
kode pos penulis post 2: 12530

Satu query dengan INNER JOIN menggantikan dua lompatan relasi. Saudara kembarnya untuk sisi banyak, hasManyThrough(), memakai pola yang sama ketika tujuan akhirnya banyak baris — misalnya seluruh riwayat revisi dari semua tulisan seorang user, yang tersimpan di tabel revisions dengan foreign key post_id:

php
// app/Models/User.php
public function revisions(): HasManyThrough
{
    return $this->hasManyThrough(Revision::class, Post::class);
}
php
$budi = User::where('email', '[email protected]')->first();

foreach ($budi->revisions as $revision) {
    echo '- ', $revision->catatan, ' (di ', $revision->post->judul, ")\n";
}

Output yang Dihasilkan:

text
- Perbaiki typo di paragraf dua (di Tips Produktif Belajar Laravel)
- Tambahkan contoh migrasi (di Tips Produktif Belajar Laravel)

Di sini konvensi bawaan cukup — hasManyThrough menebak kolom posts.user_id dan revisions.post_id dari nama model — sehingga dua argumen saja sudah benar. Kedua relasi through tetap satu query dan tidak pernah memuat model perantara ke memori; data posts dan users hanya dilewati di level SQL. Argumen kunci versi panjang maupun pendeknya dirinci di dokumentasi Eloquent Relationships .

Memuat Inverse dengan with() #

Inverse relation adalah kandidat paling sering di-eager load — daftar comment beserta nama penulisnya adalah tampilan yang hampir selalu ada. Pola with() dari One to One berlaku sama di sini:

php
use Illuminate\Support\Facades\DB;

DB::enableQueryLog();
$comments = Comment::with('user')->orderBy('id')->get();
foreach ($comments as $c) {
    echo $c->user->name, ': ', $c->isi, "\n";
}
echo 'total query: ', count(DB::getQueryLog()), "\n";

Output yang Dihasilkan:

text
Siti: Materinya sangat membantu!
Anto: Terima kasih, bang.
Budi: Videonya jelas sekali!
total query: 2

Tiga comment beserta penulisnya selesai dalam dua query — bandingkan dengan lazy loading yang akan mengulang query users untuk setiap baris. morphTo ikut ternetak dalam pola ini: Comment::with('commentable') memuat induk polymorphic semua baris sekaligus, dikelompokkan per tipe.

Dua arah relasi kini lengkap: maju lewat hasMany dan morphMany, kembali lewat belongsTo, morphTo, hingga relasi through yang menembus dua tingkat. Tapi with() yang barusan kita cicipi menyimpan banyak detail — relasi bersarang, kolom terpilih, hingga cara melarang lazy loading di development — dan chapter Eager Loading membahasnya satu per satu.