BAB 32: Hapus Data

Menghapus record dari database dengan konfirmasi dan feedback yang tepat di Laravel.

Fitur edit yang selesai di bab sebelumnya membuat data catatan bisa dikoreksi kapan saja. Tapi ada satu skenario yang belum ditangani: bagaimana jika catatan memang sudah tidak diperlukan sama sekali? Aplikasi yang tidak punya fitur hapus memaksa pengguna menumpuk data usang yang tidak lagi relevan.

Menambahkan fitur hapus di Laravel mengikuti pola yang sama dengan PUT di fitur edit — browser tidak mendukung HTTP method DELETE secara langsung dari form HTML, sehingga Laravel menggunakan teknik method spoofing yang serupa. Satu route baru, satu method di controller, dan beberapa baris di view sudah cukup untuk melengkapi kemampuan CRUD aplikasi ini.

Route DELETE

Tambahkan satu route terakhir ke routes/web.php:

// routes/web.php
<?php

use App\Http\Controllers\CatatanController;
use Illuminate\Support\Facades\Route;

Route::get('/', function () {
    return view('welcome');
});

Route::get('/catatan', [CatatanController::class, 'index'])->name('catatan.index');
Route::get('/catatan/create', [CatatanController::class, 'create'])->name('catatan.create');
Route::post('/catatan', [CatatanController::class, 'store'])->name('catatan.store');
Route::get('/catatan/{catatan}', [CatatanController::class, 'show'])->name('catatan.show');
Route::get('/catatan/{catatan}/edit', [CatatanController::class, 'edit'])->name('catatan.edit');
Route::put('/catatan/{catatan}', [CatatanController::class, 'update'])->name('catatan.update');
Route::delete('/catatan/{catatan}', [CatatanController::class, 'destroy'])->name('catatan.destroy');

Route ini menggunakan HTTP method DELETE dan URL yang sama dengan route show dan update/catatan/{catatan}. Laravel membedakan ketiganya berdasarkan HTTP method yang digunakan, bukan URL-nya. Ini adalah prinsip REST: satu URL merepresentasikan satu resource, dan method HTTP menentukan operasi yang dilakukan.

Method destroy di Controller

Tambahkan method destroy ke app/Http/Controllers/CatatanController.php:

// app/Http/Controllers/CatatanController.php
<?php

namespace App\Http\Controllers;

use App\Models\Catatan;
use Illuminate\Http\Request;

class CatatanController extends Controller
{
    public function index()
    {
        $catatans = Catatan::latest()->get();

        return view('catatan.index', compact('catatans'));
    }

    public function create()
    {
        return view('catatan.create');
    }

    public function store(Request $request)
    {
        $validated = $request->validate([
            'judul'    => 'required|string|max:255',
            'isi'      => 'required|string',
            'prioritas' => 'required|in:rendah,sedang,tinggi',
        ]);

        Catatan::create($validated);

        return redirect()->route('catatan.index')
            ->with('success', 'Catatan berhasil disimpan.');
    }

    public function show(Catatan $catatan)
    {
        return view('catatan.show', compact('catatan'));
    }

    public function edit(Catatan $catatan)
    {
        return view('catatan.edit', compact('catatan'));
    }

    public function update(Request $request, Catatan $catatan)
    {
        $validated = $request->validate([
            'judul'    => 'required|string|max:255',
            'isi'      => 'required|string',
            'prioritas' => 'required|in:rendah,sedang,tinggi',
        ]);

        $catatan->update($validated);

        return redirect()->route('catatan.show', $catatan)
            ->with('success', 'Catatan berhasil diperbarui.');
    }

    public function destroy(Catatan $catatan)
    {
        $catatan->delete();

        return redirect()->route('catatan.index')
            ->with('success', 'Catatan berhasil dihapus.');
    }
}

Method destroy adalah yang paling sederhana di antara semua method yang sudah ditulis. Tidak ada validasi input, tidak ada logika kondisional — cukup $catatan->delete() dan redirect ke halaman listing.

Ini bisa singkat karena route model binding sudah menangani kerumitannya: saat request masuk dengan URL /catatan/5, Laravel secara otomatis mencari record dengan id = 5 di tabel catatans dan menyuntikkannya ke parameter $catatan. Jika tidak ditemukan, Laravel melempar 404 sebelum destroy sempat dieksekusi. Jadi di dalam method, objek $catatan sudah pasti valid dan sudah siap dihapus.

$catatan->delete() menjalankan DELETE FROM catatans WHERE id = 5 di database, lalu mengarahkan pengguna ke halaman daftar dengan pesan konfirmasi.

Operasi delete() permanen dan tidak bisa dibatalkan. Data yang sudah dihapus tidak bisa dikembalikan kecuali ada backup atau kamu mengimplementasikan soft delete. Selalu pastikan ada konfirmasi sebelum pengguna bisa menghapus data.

Tombol Hapus di Halaman Detail

Tombol hapus ditempatkan di halaman detail, berdampingan dengan tombol Edit. Perbarui resources/views/catatan/show.blade.php:

{{-- resources/views/catatan/show.blade.php --}}
@extends('layouts.app')

@section('title', $catatan->judul)

@section('content')
    <div class="flex items-center justify-between mb-6">
        <h1 class="text-2xl font-bold text-gray-900">{{ $catatan->judul }}</h1>
        <div class="flex items-center gap-3">
            <a href="{{ route('catatan.edit', $catatan) }}" class="bg-yellow-500 text-white px-3 py-1.5 rounded text-sm font-medium hover:bg-yellow-600">
                Edit
            </a>
            <form action="{{ route('catatan.destroy', $catatan) }}" method="POST"
                  onsubmit="return confirm('Yakin ingin menghapus catatan ini?')">
                @csrf
                @method('DELETE')
                <button type="submit" class="bg-red-600 text-white px-3 py-1.5 rounded text-sm font-medium hover:bg-red-700">
                    Hapus
                </button>
            </form>
            <a href="{{ route('catatan.index') }}" class="text-sm text-gray-500 hover:text-gray-700">
                Kembali ke daftar
            </a>
        </div>
    </div>

    <div class="bg-white rounded border border-gray-200 p-6">
        <div class="mb-4">
            <span class="inline-block bg-gray-100 text-gray-700 text-xs font-medium px-2 py-1 rounded capitalize">
                Prioritas: {{ $catatan->prioritas }}
            </span>
            <span class="ml-2 text-xs text-gray-400">
                {{ $catatan->created_at->format('d M Y, H:i') }}
            </span>
        </div>

        <div class="text-gray-700 text-sm leading-relaxed whitespace-pre-wrap">
            {{ $catatan->isi }}
        </div>
    </div>
@endsection

Ada beberapa hal yang perlu diperhatikan di bagian ini.

Tombol hapus harus berupa <form>, bukan <a>. Link biasa hanya bisa mengirim request GET. Karena route destroy menggunakan method DELETE, kita butuh form dengan @method('DELETE') — teknik yang sama seperti @method('PUT') di form edit.

@method('DELETE') menghasilkan <input type="hidden" name="_method" value="DELETE">. Laravel membaca hidden input ini dan memperlakukan request seolah-olah methodnya DELETE, meskipun browser mengirimnya sebagai POST.

onsubmit="return confirm(...) adalah konfirmasi sederhana menggunakan dialog bawaan browser. Jika pengguna menekan “OK”, form dikirim. Jika menekan “Batal”, form tidak jadi dikirim. Ini perlindungan minimal yang mencegah penghapusan tidak sengaja karena klik yang salah.

Untuk konfirmasi yang lebih baik dari tampilan visual, kamu bisa mengganti confirm() dengan modal dialog menggunakan JavaScript. Tapi untuk aplikasi sederhana, confirm() sudah cukup fungsional dan tidak perlu library tambahan.

Menguji Fitur Hapus

Buka halaman detail catatan mana saja. Sekarang ada tiga aksi di header — tombol Edit kuning, tombol Hapus merah, dan link “Kembali ke daftar”.

Halaman detail catatan dengan tombol Edit kuning dan tombol Hapus merah di header

Gambar 1: Halaman detail dengan tombol Edit dan Hapus

Klik tombol Hapus merah. Browser menampilkan dialog konfirmasi dengan pesan “Yakin ingin menghapus catatan ini?”.

Dialog konfirmasi browser dengan pesan yakin ingin menghapus catatan ini dan tombol Cancel serta OK

Gambar 2: Dialog konfirmasi sebelum penghapusan

Klik “OK”. Laravel memproses request DELETE, memanggil $catatan->delete(), lalu mengarahkan ke halaman listing dengan flash message. Catatan yang baru saja dihapus tidak akan muncul lagi di daftar.

Halaman listing catatan kosong dengan flash message hijau bertuliskan catatan berhasil dihapus

Gambar 3: Flash message setelah penghapusan berhasil

CRUD Selesai

Dengan selesainya fitur hapus, aplikasi manajemen catatan sudah memiliki kemampuan CRUD yang lengkap:

  • Create — form di /catatan/create, diproses oleh store()
  • Read — listing di /catatan, detail di /catatan/{id}
  • Update — form di /catatan/{id}/edit, diproses oleh update()
  • Delete — tombol di halaman detail, diproses oleh destroy()

Empat operasi ini adalah fondasi dari hampir semua aplikasi web berbasis data. Pola yang sudah dipakai sepanjang bab-bab studi kasus ini — route, controller, view, model binding, validasi, flash message — akan terus berulang di fitur-fitur berikutnya dengan variasi yang semakin kompleks.

Tapi ada satu masalah yang langsung terasa setelah fitur hapus ini diuji: setelah menghapus beberapa catatan, database jadi kosong lagi. Mengisi ulang data lewat form satu per satu membuang waktu. Di bab berikutnya, kita akan menyelesaikan masalah ini dengan seeder dan factory — dua alat yang membuat database bisa terisi otomatis dengan data realistis hanya dari satu perintah.

Referensi

  1. 1Eloquent: Deleting Models — Laravel 12.x Documentation
  2. 2Routing: Form Method Spoofing — Laravel 12.x Documentation