BelongsToMany: Relasi Banyak-ke-Banyak
Post punya banyak tag, tag punya banyak post: tabel pivot.
Kapan Satu-ke-Banyak Tidak Cukup?
Satu post bisa punya banyak tag, dan satu tag menempel di banyak post. Tidak ada sisi yang "memiliki" sisi lain, jadi satu foreign key tidak bisa mewakili hubungan ini. Solusinya adalah tabel pivot di tengah yang menyimpan pasangan id. Ini relasi many-to-many, dan belongsToMany adalah caranya di Eloquent.
Tabel Pivot Mengikuti Konvensi
Konvensi nama Laravel: dua nama model singular, urut alfabet, snake_case. Post + Tag menjadi post_tag.
// database/migrations/2026_10_08_000002_create_post_tag_table.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('post_tag', function (Blueprint $table) {
$table->foreignId('post_id')->constrained()->cascadeOnDelete();
$table->foreignId('tag_id')->constrained()->cascadeOnDelete();
$table->primary(['post_id', 'tag_id']);
});
}
};Primary key komposit mencegah pasangan ganda di level database. cascadeOnDelete memastikan baris pivot ikut terhapus saat post atau tag dihapus, jadi tidak ada sampah yatim piatu.
Definisi di Kedua Model
// app/Models/Post.php
public function tags()
{
return $this->belongsToMany(Tag::class);
}// app/Models/Tag.php
public function posts()
{
return $this->belongsToMany(Post::class);
}Laravel menebak nama tabel pivot dari kedua model, jadi selama mengikuti konvensi tidak perlu argumen tambahan. Hati-hati dengan copy-paste: sisi Tag harus menunjuk Post::class, bukan Tag::class. Ini kesalahan klasik yang membuat relasi mengarah ke tabel yang salah.
Mengelola: attach, detach, sync
// app/Http/Controllers/PostController.php
$post->tags()->attach([1, 2]); // tambah pasangan
$post->tags()->detach([2]); // lepas pasangan
$post->tags()->sync([1, 3]); // samakan persis: tambah 3, lepas 2sync() adalah andalan untuk form checkbox: kirim array id yang dicentang dari form, Laravel mengurus penambahan dan pelepasannya dalam satu operasi. attach() pada pasangan yang sudah ada melempar duplicate key error, jadi untuk operasi yang idempoten (aman dijalankan berulang) pakai syncWithoutDetaching().
Jebakan umum: memanggil ketiga method ini pada property ($post->tags) bukan method ($post->tags()). Property mengembalikan Collection yang tidak punya method attach/detach/sync. Selalu pakai kurung untuk operasi tulis pada relasi many-to-many.
Catatan teknis: Hasil attach/detach/sync tidak me-refresh relasi $post->tags yang mungkin sudah di-load sebelumnya di request yang sama. Kalau setelah sync kamu langsung menampilkan $post->tags, datanya basi. Panggil $post->load('tags') atau $post->refresh() untuk memuat ulang. Untuk mengecek keanggotaan tanpa memuat semua, pakai $post->tags()->where('tags.id', $id)->exists().
toggle() untuk Fitur Like dan Bookmark
Untuk relasi "suka atau tidak" seperti like dan bookmark, ada method toggle() yang membalik status secara otomatis:
// app/Http/Controllers/LikeController.php
$post->likedBy()->toggle(auth()->id());
// sudah like -> di-detach; belum like -> di-attachSatu baris menggantikan if-else dengan exists() manual. Method ini mengembalikan array berisi key 'attached' dan 'detached' sehingga kamu tahu apa yang baru saja terjadi:
$hasil = $post->likedBy()->toggle(auth()->id());
$menyukai = !empty($hasil['attached']); // true kalau baru saja likeCocok untuk endpoint API toggle like yang perlu me-return JSON berisi status terbaru ke frontend.
Tantangan
Tagging Post
Buat tabel pivot post_tag + relasi belongsToMany di kedua model. Via tinker: attach 3 tag ke 1 post, tampilkan nama-nama tag-nya, lalu sync jadi 2 tag.