BAB 30: Detail Data
Menampilkan detail satu record berdasarkan ID dengan route model binding di Laravel.
Halaman listing yang sudah jadi di bab sebelumnya hanya menampilkan judul dan prioritas tiap catatan. Isi catatan yang sebenarnya belum bisa dibaca karena memang tidak ada halaman untuk itu. Setiap baris di daftar hanyalah nama tanpa tujuan — tidak ada yang bisa diklik.
Bab ini membangun halaman detail: halaman yang menampilkan satu catatan secara lengkap, termasuk isi, prioritas, dan waktu pembuatannya. Di sini kamu akan bertemu dengan route model binding, salah satu fitur Laravel yang membuat pengambilan data berdasarkan ID menjadi sangat ringkas.
Route Model Binding
Cara paling biasa untuk mengambil satu data berdasarkan ID adalah seperti ini:
public function show($id)
{
$catatan = Catatan::findOrFail($id);
return view('catatan.show', compact('catatan'));
}
Laravel punya cara yang lebih elegan. Cukup ganti tipe parameter di method controller dengan nama model-nya:
public function show(Catatan $catatan)
{
return view('catatan.show', compact('catatan'));
}
Itulah route model binding. Laravel melihat bahwa parameter $catatan bertipe Catatan, lalu secara otomatis mencari record di tabel dengan ID yang cocok dari URL. Jika ID tidak ditemukan, Laravel langsung mengembalikan response 404 — tanpa perlu findOrFail() manual.
Satu syarat agar ini bekerja: nama parameter di route harus identik dengan nama variabel di method controller. Route /catatan/{catatan} menghasilkan parameter bernama catatan, dan method controller pun menerima Catatan $catatan. Keduanya sama — ini bukan kebetulan.
Menambahkan Route
Buka routes/web.php dan tambahkan satu route baru di bawah route store:
// 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');
Perhatikan posisi route ini — ditulis setelah /catatan/create, bukan sebelumnya. Jika /catatan/{catatan} ditulis lebih dulu, string create akan terbaca sebagai nilai parameter {catatan}, dan request ke /catatan/create akan ditangkap oleh route show alih-alih route create.
Urutan route di web.php menentukan route mana yang cocok lebih dulu. Route dengan segmen dinamis seperti {catatan} harus selalu ditulis setelah route dengan segmen statis yang bentuknya mirip.
Method show di Controller
Tambahkan method show 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'));
}
}
Method show hanya dua baris: terima objek $catatan yang sudah di-resolve Laravel, lalu kirim ke view. Tidak ada query manual, tidak ada pengecekan null, tidak ada try/catch. Semua itu ditangani framework.
View Detail
Buat file baru 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>
<a href="{{ route('catatan.index') }}" class="text-sm text-gray-500 hover:text-gray-700">
Kembali ke daftar
</a>
</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
@section('title', $catatan->judul) menggunakan judul catatan sebagai judul halaman, bukan string statis. Ini membuat tab browser menampilkan nama catatan yang sedang dibaca — detail kecil yang membuat aplikasi terasa lebih profesional.
$catatan->created_at->format('d M Y, H:i') memformat timestamp menjadi string yang lebih ramah baca seperti “17 Mar 2026, 09
format() ini tersedia karena Eloquent secara otomatis mengonversi kolom created_at menjadi objek Carbon — library tanggal dan waktu yang sudah disertakan di Laravel.
whitespace-pre-wrap pada div isi memastikan baris baru yang diketik pengguna di textarea saat membuat catatan tetap tampil sebagai baris baru di halaman ini. Tanpa class ini, semua teks akan digabung menjadi satu blok.
Update View Index
View listing perlu diperbarui agar judul setiap catatan bisa diklik dan mengarah ke halaman detail. Buka resources/views/catatan/index.blade.php dan ubah bagian loop @foreach:
{{-- resources/views/catatan/index.blade.php --}}
@extends('layouts.app')
@section('title', 'Daftar Catatan')
@section('content')
<div class="flex items-center justify-between mb-6">
<h1 class="text-2xl font-bold text-gray-900">Catatan Saya</h1>
<a href="{{ route('catatan.create') }}" class="bg-blue-600 text-white px-4 py-2 rounded text-sm font-medium hover:bg-blue-700">
+ Catatan Baru
</a>
</div>
@if ($catatans->isEmpty())
<p class="text-gray-500">Belum ada catatan. Mulai buat catatan pertamamu.</p>
@else
<div class="space-y-3">
@foreach ($catatans as $catatan)
<div class="bg-white p-4 rounded border border-gray-200">
<a href="{{ route('catatan.show', $catatan) }}" class="font-medium text-gray-900 hover:text-blue-600">
{{ $catatan->judul }}
</a>
<div class="mt-1">
<span class="text-xs text-gray-500 capitalize">Prioritas: {{ $catatan->prioritas }}</span>
</div>
</div>
@endforeach
</div>
@endif
@endsection
Perubahan ada pada tag <h2> yang diganti menjadi <a> dengan atribut href="{{ route('catatan.show', $catatan) }}". Argument kedua dari helper route() di sini adalah objek $catatan secara langsung, bukan $catatan->id. Laravel cukup pintar untuk mengambil nilai primary key dari objek model secara otomatis.
Melempar objek model langsung ke helper route() adalah idiom yang umum di Laravel. Tidak perlu menulis route('catatan.show', ['catatan' => $catatan->id]) — cukup route('catatan.show', $catatan).
Menguji Halaman Detail
Buka http://localhost:8000/catatan. Judul-judul catatan yang sebelumnya hanya teks biasa sekarang berubah menjadi link yang bisa diklik.
Klik salah satu judul untuk membuka halaman detailnya. URL yang terbentuk akan seperti /catatan/1, /catatan/2, dan seterusnya. Angka di bagian akhir adalah ID record yang diambil dari database.
Coba akses URL dengan ID yang tidak ada, misalnya /catatan/999. Laravel langsung mengembalikan halaman 404 tanpa perlu kode pengecekan manual — ini adalah hasil dari route model binding yang bekerja secara otomatis.
Halaman sudah berfungsi dengan baik — satu catatan tampil lengkap dengan judul, badge prioritas, waktu pembuatan, dan isi teksnya. Sekarang pengguna bisa membaca isi catatan, tapi belum bisa mengubahnya jika ada yang perlu dikoreksi. Itu yang akan dibangun di bab berikutnya: form untuk mengedit catatan yang sudah ada.