Aplikasi kita bertambah besar: selain menampilkan post, sekarang ada konten video — dan pengguna ingin berkomentar di keduanya. Kita bisa membuat dua tabel terpisah, post_comments dan video_comments, tetapi keduanya pasti berisi kolom yang sama persis dan setiap fitur komentar harus ditulis dua kali. Polymorphic relationship menawarkan jalan ketiga: satu tabel comments yang barisnya bisa dimiliki oleh post maupun video. Namanya berasal dari kata Yunani banyak bentuk — satu relasi, banyak tipe induk. Chapter ini membangunnya dari struktur kolom sampai query dua arah, lalu melanjutkan ke versi many-to-many-nya untuk tag.
Masalah: Satu Anak, Banyak Jenis Induk #
Kekurangan foreign key tunggal menjadi jelas begitu kita membayangkan skema comments biasa. Kolom post_id hanya bisa menunjuk post; comment pada video tidak punya tempat untuk menyimpan videonya. Menambah kolom video_id di samping post_id menyelesaikan dua induk, tetapi begitu konten bertambah jenis ketiga — misalnya podcast — kita kembali mengedit skema. Polymorphic menjawab masalah ini dengan mengganti satu foreign key menjadi sepasang kolom: satu menyimpan tipe induk, satu menyimpan id induk.
Struktur Kolom commentable #
Schema builder menyediakan morphs() untuk membangun pasangan kolom tersebut dalam satu panggilan:
Schema::create('comments', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
$table->morphs('commentable');
$table->text('isi');
$table->timestamps();
});Argumen 'commentable' menjadi prefix kedua kolom: commentable_type untuk tipe induk dan commentable_id untuk id-nya, dilengkapi index gabungan. Mari kita isi tabelnya dan lihat apa yang benar-benar tersimpan:
foreach (Comment::orderBy('id')->get(['id', 'commentable_type', 'commentable_id']) as $c) {
echo 'comment ', $c->id, ': type=', $c->commentable_type, ', id=', $c->commentable_id, "\n";
}Output yang Dihasilkan:
comment 1: type=App\Models\Post, id=1
comment 2: type=App\Models\Post, id=1
comment 3: type=App\Models\Video, id=1Nilai commentable_type adalah nama class lengkap induknya. Dua comment pertama dimiliki post dengan id 1, comment ketiga dimiliki video dengan id 1 — perhatikan bahwa id keduanya boleh sama karena tipe yang membedakan. Pasangan kolom ini semacam foreign key yang bisa menunjuk ke tabel mana pun.
morphMany dan morphTo #
Definisi relasinya nyaris simetris di semua induk. Model Post dan Video sama-sama memakai morphMany(), dan Comment mendefinisikan jalan baliknya lewat morphTo():
// app/Models/Post.php dan app/Models/Video.php
public function comments(): MorphMany
{
return $this->morphMany(Comment::class, 'commentable');
}
// app/Models/Comment.php
public function commentable(): MorphTo
{
return $this->morphTo();
}Argumen kedua 'commentable' adalah nama prefix kolom — induk dan anak harus sepakat memakai nama yang sama. SQL yang dibangun relasi ini memperlihatkan cara kerja sepasang kolom tadi, misalnya dari sisi post:
$post1 = Post::where('judul', 'Tips Produktif Belajar Laravel')->first();
echo 'morphMany SQL: [', $post1->comments()->toSql(), "]\n";
echo 'bindings: ';
var_export($post1->comments()->getBindings());Output yang Dihasilkan:
morphMany SQL: [select * from "comments" where "comments"."commentable_type" = ? and "comments"."commentable_id" = ? and "comments"."commentable_id" is not null]
bindings: array (
0 => 'App\\Models\\Post',
1 => 1,
)Query anak di-filter dua syarat sekaligus: tipe induk dan id induk. Mengambil dan menulis datanya lalu berjalan seperti relasi lain yang sudah kita kenal:
foreach ($post1->comments as $c) {
echo '[post] ', $c->isi, "\n";
}
foreach ($video->comments as $c) {
echo '[video] ', $c->isi, "\n";
}
$baru = $video->comments()->create(['user_id' => $siti->id, 'isi' => 'Saya sudah mencoba langkahnya!']);
echo 'tersimpan id ', $baru->id, ' dengan commentable_type ', $baru->commentable_type, ' dan commentable_id ', $baru->commentable_id, "\n";Output yang Dihasilkan:
[post] Materinya sangat membantu!
[post] Terima kasih, bang.
[video] Videonya jelas sekali!
tersimpan id 4 dengan commentable_type App\Models\Video dan commentable_id 1Method create() pada relasi mengisi kedua kolom commentable_* secara otomatis dari model induk yang memanggilnya — kita tidak pernah menulis nama class secara manual. Inverse-nya bahkan lebih menarik karena morphTo() harus melayani dua jenis induk sekaligus:
foreach (Comment::orderBy('id')->get() as $c) {
echo $c->isi, ' -> ', $c->commentable::class, ' (', $c->commentable->judul, ")\n";
}Output yang Dihasilkan:
Materinya sangat membantu! -> App\Models\Post (Tips Produktif Belajar Laravel)
Terima kasih, bang. -> App\Models\Post (Tips Produktif Belajar Laravel)
Videonya jelas sekali! -> App\Models\Video (Instalasi Laravel 13 dari Nol)Ekspresi $c->commentable membaca commentable_type, memuat class yang sesuai, lalu mengembalikan model Post atau Video tanpa satu pun pernyataan if. Satu method berfungsi untuk semua induk — inilah kekuatan sebenarnya dari pola ini.
morphOne: Satu Anak Terpilih #
Terkadang dari sekian banyak anak kita hanya butuh satu — seperti latestComment() di chapter One to Many
, versi polymorphic-nya memakai morphOne():
public function firstComment(): MorphOne
{
return $this->morphOne(Comment::class, 'commentable')->oldestOfMany();
}echo $post1->firstComment->isi, "\n";Output yang Dihasilkan:
Materinya sangat membantu!Relasi ini memilih comment pertama milik post lewat subquery agregat MIN(id). Kombinasi morphOne() dengan ofMany() memberi kita “yang terpilih” dari sekumpulan anak polymorphic — satu thumbnail untuk satu galeri, satu harga aktif untuk satu produk. Seluruh varian relasi polymorphic dirinci di dokumentasi Eloquent Relationships
.
morphToMany dan morphedByMany untuk Tag #
Polymorphic versi many-to-many menjawab kebutuhan yang berbeda: post dan video sama-sama ingin bertag, dan satu tag boleh dipakai keduanya. Strukturnya menggabungkan pivot table dari Many to Many dengan pasangan kolom polymorphic:
Schema::create('tags', function (Blueprint $table) {
$table->id();
$table->string('nama')->unique();
$table->timestamps();
});
Schema::create('taggables', function (Blueprint $table) {
$table->foreignId('tag_id')->constrained()->cascadeOnDelete();
$table->morphs('taggable');
$table->unique(['tag_id', 'taggable_type', 'taggable_id']);
});Tabel taggables kini menyimpan sepasang kolom taggable_type dan taggable_id menggantikan satu foreign key model kedua — isi barisnya misalnya tag_id 1, taggable_type App\Models\Post, taggable_id 1. Dari sisi konten, relasinya adalah morphToMany(); dari sisi tag, inverse-nya adalah morphedByMany():
// app/Models/Post.php dan app/Models/Video.php
public function tags(): MorphToMany
{
return $this->morphToMany(Tag::class, 'taggable');
}
// app/Models/Tag.php
public function posts(): MorphToMany
{
return $this->morphedByMany(Post::class, 'taggable');
}
public function videos(): MorphToMany
{
return $this->morphedByMany(Video::class, 'taggable');
}Mari kita jalankan dua arahnya, termasuk menautkan tag baru lewat attach() seperti biasa:
foreach ($post1->tags as $tag) {
echo '[post1] tag: ', $tag->nama, "\n";
}
$video->tags()->attach(2);
echo 'setelah attach Tutorial ke video: ', $video->tags()->orderBy('nama')->pluck('nama')->implode(', '), "\n";
$laravel = Tag::where('nama', 'Laravel')->first();
echo 'post bertag Laravel: ', $laravel->posts->pluck('judul')->implode(' | '), "\n";
echo 'video bertag Laravel: ', $laravel->videos->pluck('judul')->implode(' | '), "\n";Output yang Dihasilkan:
[post1] tag: Laravel
[post1] tag: Tutorial
setelah attach Tutorial ke video: Database, Laravel, Tutorial
post bertag Laravel: Tips Produktif Belajar Laravel
video bertag Laravel: Instalasi Laravel 13 dari NolSatu tag Laravel terbaca dari post maupun video melalui tabel taggables yang sama, dan inverse morphedByMany() membuat setiap sisi bisa menelusuri mitranya. Seluruh relasi section ini hingga kini punya pola yang sama: satu arah maju, satu arah balik. Bab berikutnya merangkum pola dua arah tersebut secara sistematis, termasuk relasi yang menembus dua tabel sekaligus — semuanya di Inverse Relationships
.