BAB 29: Insert Data
Membangun form dan logika untuk menyimpan data baru ke database menggunakan Laravel.
Halaman listing di bab sebelumnya sudah berfungsi, tapi masih kosong karena belum ada cara untuk memasukkan data. Selama pesan “Belum ada catatan” itu ada, fitur listing tidak bisa dibuktikan benar-benar bekerja. Jadi langkah berikutnya adalah membangun form untuk menambahkan catatan baru.
Proses insert data di Laravel melibatkan dua route, dua method controller, dan satu view form. Ini adalah pola yang akan kamu temukan berulang kali: satu untuk menampilkan form kosong, satu lagi untuk menangani pengiriman form dan menyimpan data.
Dua Route untuk Satu Fitur
Buka routes/web.php dan tambahkan dua route baru:
// 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');
Perhatikan perbedaan method HTTP-nya: /catatan/create menggunakan GET karena hanya menampilkan form, sementara /catatan untuk menyimpan data menggunakan POST. Keduanya mengarah ke method yang berbeda di controller yang sama.
Urutan penulisan route di web.php penting. Route /catatan/create harus ditulis sebelum route resource lain yang mungkin menangkap /catatan/{id} — karena Laravel mencocokkan route dari atas ke bawah, dan string create bisa terbaca sebagai parameter {id} jika route wildcard lebih dulu didefinisikan.
Method Controller: create dan store
Buka app/Http/Controllers/CatatanController.php dan tambahkan dua method baru:
// 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.');
}
}
Method create sangat sederhana — hanya mengembalikan view form. Semua logika sebenarnya ada di store.
Di store, ada tiga langkah yang terjadi secara berurutan. Pertama, $request->validate() memeriksa input dari form. Jika salah satu aturan dilanggar, Laravel secara otomatis mengalihkan pengguna kembali ke form dengan pesan error dan nilai input sebelumnya — tanpa kita perlu menulis kode redirect manual.
Kedua, kalau semua validasi lolos, Catatan::create($validated) menyimpan data ke database. Variabel $validated berisi hanya field yang sudah lolos validasi — bukan seluruh $request->all(). Ini penting untuk keamanan: kita tidak mau field asing ikut tersimpan ke database.
Kamu mungkin bertanya-tanya: bagaimana dengan user_id? Field itu tidak ada di form dan tidak divalidasi, tapi tabel catatan punya kolom itu. Jawabannya ada di migration yang kita tulis di BAB 26 — kolom user_id didefinisikan sebagai nullable(), sehingga boleh kosong. Untuk saat ini kita biarkan NULL dulu. Nanti di BAB 34 saat proteksi route dengan autentikasi sudah ditambahkan, user_id akan diisi otomatis dari auth()->id().
Ketiga, redirect ke halaman index dengan pesan sukses melalui session flash. Pesan ini sudah disiapkan di layouts/app.blade.php sejak bab sebelumnya — kini saatnya dipakai.
View Form
Buat file baru resources/views/catatan/create.blade.php:
{{-- resources/views/catatan/create.blade.php --}}
@extends('layouts.app')
@section('title', 'Catatan Baru')
@section('content')
<div class="flex items-center justify-between mb-6">
<h1 class="text-2xl font-bold text-gray-900">Catatan Baru</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">
<form action="{{ route('catatan.store') }}" method="POST">
@csrf
<div class="mb-4">
<label for="judul" class="block text-sm font-medium text-gray-700 mb-1">
Judul
</label>
<input
type="text"
id="judul"
name="judul"
value="{{ old('judul') }}"
class="w-full border border-gray-300 rounded px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 @error('judul') border-red-400 @enderror"
placeholder="Judul catatan"
>
@error('judul')
<p class="mt-1 text-xs text-red-600">{{ $message }}</p>
@enderror
</div>
<div class="mb-4">
<label for="isi" class="block text-sm font-medium text-gray-700 mb-1">
Isi
</label>
<textarea
id="isi"
name="isi"
rows="5"
class="w-full border border-gray-300 rounded px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 @error('isi') border-red-400 @enderror"
placeholder="Tulis isi catatan di sini..."
>{{ old('isi') }}</textarea>
@error('isi')
<p class="mt-1 text-xs text-red-600">{{ $message }}</p>
@enderror
</div>
<div class="mb-6">
<label for="prioritas" class="block text-sm font-medium text-gray-700 mb-1">
Prioritas
</label>
<select
id="prioritas"
name="prioritas"
class="w-full border border-gray-300 rounded px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 @error('prioritas') border-red-400 @enderror"
>
<option value="">Pilih prioritas</option>
<option value="rendah" {{ old('prioritas') === 'rendah' ? 'selected' : '' }}>Rendah</option>
<option value="sedang" {{ old('prioritas') === 'sedang' ? 'selected' : '' }}>Sedang</option>
<option value="tinggi" {{ old('prioritas') === 'tinggi' ? 'selected' : '' }}>Tinggi</option>
</select>
@error('prioritas')
<p class="mt-1 text-xs text-red-600">{{ $message }}</p>
@enderror
</div>
<button
type="submit"
class="bg-blue-600 text-white px-4 py-2 rounded text-sm font-medium hover:bg-blue-700"
>
Simpan Catatan
</button>
</form>
</div>
@endsection
Ada beberapa hal yang perlu diperhatikan di form ini.
@csrf adalah directive Blade yang menyisipkan hidden input berisi token keamanan. Laravel memeriksa token ini setiap kali ada request POST — jika token tidak ada atau tidak cocok, request ditolak dengan error 419. Ini melindungi aplikasi dari serangan CSRF (Cross-Site Request Forgery).
{{ old('judul') }} mengisi kembali nilai input jika form pernah dikirim tapi validasi gagal. Tanpa old(), pengguna harus mengisi ulang semua field dari awal setiap kali ada error — pengalaman yang sangat buruk.
@error('judul') ... @enderror adalah directive untuk menampilkan pesan error validasi dari field tertentu. $message di dalamnya berisi teks error yang di-generate Laravel sesuai aturan validasi yang dilanggar.
Pada field select, {{ old('prioritas') === 'rendah' ? 'selected' : '' }} memastikan pilihan yang sebelumnya dipilih tetap terpilih saat form kembali setelah error validasi.
Update View Index
Halaman listing perlu tombol untuk mengakses form. Update resources/views/catatan/index.blade.php:
{{-- 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">
<h2 class="font-medium text-gray-900">{{ $catatan->judul }}</h2>
<span class="text-xs text-gray-500 capitalize">
Prioritas: {{ $catatan->prioritas }}
</span>
</div>
@endforeach
</div>
@endif
@endsection
Perubahan dari bab sebelumnya: heading <h1> dipindah ke dalam flex container bersama tombol “Catatan Baru”. Tombol ini mengarah ke route('catatan.create') yang baru saja kita daftarkan.
Alur Request POST
Penting untuk memahami bagaimana Laravel menangani pengiriman form secara keseluruhan:
Browser klik "Simpan Catatan"
--> POST /catatan
--> CatatanController@store
--> validate() -- gagal? redirect kembali ke form dengan errors
--> Catatan::create($validated) -- simpan ke database
--> redirect ke /catatan dengan session flash 'success'
--> halaman index menampilkan pesan sukses
Redirect setelah POST adalah pola yang sangat penting. Jika store langsung mengembalikan view tanpa redirect, pengguna yang menekan F5 di browser akan mengirim ulang form yang sama — dan catatan yang sama tersimpan dua kali. Dengan redirect, F5 hanya memuat ulang halaman GET /catatan, bukan mengirim ulang POST.
Pola ini dikenal sebagai Post/Redirect/Get (PRG). Hampir semua framework web modern menganjurkan pola ini untuk mencegah double submission.
Menguji Form
Buka http://localhost:8000/catatan — tombol ”+ Catatan Baru” sudah muncul di pojok kanan atas halaman listing.
Klik tombol itu — form kosong muncul di /catatan/create.
Isi semua field, lalu klik “Simpan Catatan”.
Setelah submit berhasil, browser diarahkan ke /catatan. Pesan hijau “Catatan berhasil disimpan.” muncul di atas, dan catatan baru sudah masuk ke daftar.
Halaman listing sekarang punya data sungguhan. Langkah berikutnya adalah membangun halaman detail agar pengguna bisa membaca isi lengkap setiap catatan — tidak hanya judul dan prioritasnya.