LaravelEloquentRelasiMenengah5 mnt baca

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.

php
// 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

php
// app/Models/Post.php
public function tags()
{
    return $this->belongsToMany(Tag::class);
}
php
// 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

php
// 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 2

sync() 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:

php
// app/Http/Controllers/LikeController.php
$post->likedBy()->toggle(auth()->id());
// sudah like -> di-detach; belum like -> di-attach

Satu baris menggantikan if-else dengan exists() manual. Method ini mengembalikan array berisi key 'attached' dan 'detached' sehingga kamu tahu apa yang baru saja terjadi:

php
$hasil = $post->likedBy()->toggle(auth()->id());
$menyukai = !empty($hasil['attached']); // true kalau baru saja like

Cocok 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.