BAB 12: Validasi Input
Memvalidasi input pengguna menggunakan sistem validasi bawaan Laravel dan menampilkan pesan error.
Form catatan.create yang kita bangun di bab-bab sebelumnya sudah bisa menerima input dan menampilkan pesan error lewat @error di Blade. Tapi belum ada kode yang benar-benar memeriksa apakah input tersebut valid — apakah judul terisi, apakah panjangnya masuk akal, apakah pengguna tidak mengirim data kosong. Tanpa validasi, data sampah masuk ke database, dan aplikasi bisa berperilaku tidak terduga.
Laravel menyediakan sistem validasi yang bisa menangani semua itu dengan sedikit kode. Data yang gagal validasi otomatis dikembalikan ke form beserta pesan error, sementara data valid langsung tersedia untuk diproses.
Validasi di Controller
Cara tercepat adalah memanggil $request->validate() langsung di dalam method controller. Jika ada rule yang dilanggar, Laravel secara otomatis mengirim ulang pengguna ke halaman sebelumnya beserta pesan error dan input lama — kita tidak perlu menulis satu pun baris redirect manual.
<?php
// app/Http/Controllers/CatatanController.php
namespace App\Http\Controllers;
use App\Models\Catatan;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
class CatatanController extends Controller
{
public function store(Request $request): RedirectResponse
{
$data = $request->validate([
'judul' => 'required|string|min:3|max:200',
'isi' => 'required|string|min:10',
'prioritas' => 'required|in:rendah,sedang,tinggi',
]);
Catatan::create($data);
return redirect()->route('catatan.index')
->with('sukses', 'Catatan berhasil dibuat.');
}
}
validate() mengembalikan hanya data yang ada di rules — bukan seluruh request. Ini berarti field tambahan yang tidak ada di rules tidak akan ikut tersimpan, bahkan jika pengguna mengirimnya secara paksa. Pola ini disebut mass assignment protection secara implisit.
Rules yang Sering Dipakai
Laravel menyediakan lebih dari 90 validation rules bawaan. Rules dipisahkan dengan | dalam format string, atau disusun sebagai array jika ingin lebih rapi.
Beberapa rules paling sering digunakan dalam aplikasi catatan kita:
| Rule | Kegunaan |
|---|---|
required | Field wajib ada dan tidak boleh kosong |
nullable | Field boleh kosong atau null |
string | Nilai harus bertipe string |
integer | Nilai harus bilangan bulat |
min:N | Panjang string minimal N karakter (atau nilai numerik minimal N) |
max:N | Panjang string maksimal N karakter |
email | Nilai harus format alamat email valid |
in:a,b,c | Nilai harus salah satu dari daftar yang ditentukan |
unique:tabel,kolom | Nilai harus unik di database |
exists:tabel,kolom | Nilai harus ada di database |
confirmed | Nilai harus sama dengan field _confirmation-nya |
Ketika menggunakan beberapa rules sekaligus, tulis sebagai array jika lebih dari tiga rules — lebih mudah dibaca:
$request->validate([
'judul' => [
'required',
'string',
'min:3',
'max:200',
'unique:catatan,judul',
],
'isi' => ['required', 'string', 'min:10'],
'prioritas' => ['required', 'in:rendah,sedang,tinggi'],
]);
Form Request
Saat logika validasi semakin kompleks, menaruh semua rules di dalam method controller membuat kode sulit dibaca. Form Request adalah cara Laravel untuk memindahkan validasi ke class tersendiri.
Buat Form Request dengan Artisan:
php artisan make:request SimpanCatatanRequest
File baru muncul di app/Http/Requests/SimpanCatatanRequest.php:
<?php
// app/Http/Requests/SimpanCatatanRequest.php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class SimpanCatatanRequest extends FormRequest
{
public function authorize(): bool
{
// Kembalikan true jika pengguna boleh melakukan aksi ini
return auth()->check();
}
public function rules(): array
{
return [
'judul' => ['required', 'string', 'min:3', 'max:200'],
'isi' => ['required', 'string', 'min:10'],
'prioritas' => ['required', 'in:rendah,sedang,tinggi'],
];
}
public function messages(): array
{
return [
'judul.required' => 'Judul catatan tidak boleh kosong.',
'judul.min' => 'Judul minimal 3 karakter.',
'judul.max' => 'Judul terlalu panjang, maksimal 200 karakter.',
'isi.required' => 'Isi catatan tidak boleh kosong.',
'isi.min' => 'Isi catatan terlalu pendek, minimal 10 karakter.',
'prioritas.in' => 'Pilih prioritas yang valid: rendah, sedang, atau tinggi.',
];
}
}
Method authorize() adalah gate pertama — jika mengembalikan false, request langsung ditolak dengan HTTP 403. Gunakan ini untuk memastikan pengguna yang bukan pemilik catatan tidak bisa mengedit milik orang lain.
Controller sekarang jauh lebih bersih:
<?php
// app/Http/Controllers/CatatanController.php
use App\Http\Requests\SimpanCatatanRequest;
class CatatanController extends Controller
{
public function store(SimpanCatatanRequest $request): RedirectResponse
{
// Validasi sudah dijalankan oleh Form Request
// $request->validated() hanya berisi data yang lolos validasi
Catatan::create($request->validated());
return redirect()->route('catatan.index')
->with('sukses', 'Catatan berhasil dibuat.');
}
}
Laravel secara otomatis menjalankan Form Request sebelum method controller dieksekusi — jika validasi gagal, controller bahkan tidak sempat dijalankan.
Buat Form Request terpisah untuk operasi yang berbeda: SimpanCatatanRequest untuk store() dan PerbaruiCatatanRequest untuk update(). Rules-nya sering berbeda — misalnya unique di store() perlu di-ignore saat update().
Validasi untuk Update
Saat memperbarui data yang sudah ada, rule unique perlu sedikit modifikasi. Judul catatan yang diedit boleh sama dengan judul lama miliknya sendiri, tapi tidak boleh sama dengan judul catatan lain.
<?php
// app/Http/Requests/PerbaruiCatatanRequest.php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class PerbaruiCatatanRequest extends FormRequest
{
public function authorize(): bool
{
// Pastikan yang mengedit adalah pemilik catatan
return $this->route('catatan')->user_id === auth()->id();
}
public function rules(): array
{
$catatanId = $this->route('catatan')->id;
return [
'judul' => [
'required',
'string',
'min:3',
'max:200',
Rule::unique('catatan', 'judul')->ignore($catatanId),
],
'isi' => ['required', 'string', 'min:10'],
'prioritas' => ['required', 'in:rendah,sedang,tinggi'],
];
}
}
Rule::unique()->ignore($id) memberitahu Laravel untuk mengabaikan baris dengan id tersebut saat memeriksa keunikan — sehingga pengguna bisa menyimpan catatan dengan judul yang sama selama itu miliknya sendiri.
Rules Kondisional
Ada situasi di mana sebuah field hanya wajib diisi jika field lain memiliki nilai tertentu. Misalnya, field jadwal_kirim hanya relevan jika status catatan adalah terjadwal.
public function rules(): array
{
return [
'judul' => ['required', 'string', 'max:200'],
'isi' => ['required', 'string'],
'status' => ['required', 'in:draft,terjadwal,diterbitkan'],
'jadwal_kirim' => ['required_if:status,terjadwal', 'nullable', 'date', 'after:now'],
];
}
required_if:status,terjadwal artinya: field ini wajib diisi hanya jika status bernilai terjadwal. Kita tetap tambahkan nullable agar tidak error saat field ini kosong pada kasus lain.
Jangan lupa tambahkan nullable bersamaan dengan rules kondisional seperti required_if. Tanpa nullable, validasi tipe data (misalnya date) tetap berjalan meskipun field kosong.
Menampilkan Error di View
Bab 11 sudah memperkenalkan @error untuk menampilkan error per field. Sekarang kita sempurnakan form dengan pola yang konsisten:
{{-- resources/views/catatan/create.blade.php --}}
@extends('layouts.app')
@section('judul', 'Buat Catatan Baru')
@section('konten')
<div class="container">
<h1>Buat Catatan Baru</h1>
<form method="POST" action="{{ route('catatan.store') }}">
@csrf
<div class="field @error('judul') field-error @enderror">
<label for="judul">Judul</label>
<input
type="text"
id="judul"
name="judul"
value="{{ old('judul') }}"
placeholder="Masukkan judul catatan..."
>
@error('judul')
<span class="pesan-error">{{ $message }}</span>
@enderror
</div>
<div class="field @error('isi') field-error @enderror">
<label for="isi">Isi Catatan</label>
<textarea
id="isi"
name="isi"
rows="8"
placeholder="Tulis isi catatan di sini..."
>{{ old('isi') }}</textarea>
@error('isi')
<span class="pesan-error">{{ $message }}</span>
@enderror
</div>
<div class="field @error('prioritas') field-error @enderror">
<label for="prioritas">Prioritas</label>
<select id="prioritas" name="prioritas">
<option value="">-- Pilih Prioritas --</option>
@foreach (['rendah', 'sedang', 'tinggi'] as $opsi)
<option
value="{{ $opsi }}"
@selected(old('prioritas') === $opsi)
>
{{ ucfirst($opsi) }}
</option>
@endforeach
</select>
@error('prioritas')
<span class="pesan-error">{{ $message }}</span>
@enderror
</div>
<button type="submit" class="tombol tombol-utama">
Simpan Catatan
</button>
</form>
</div>
@endsection
old('judul') — yang kita pakai di Bab 11 — mengambil nilai yang dikirim sebelum validasi gagal. Tanpa ini, semua field akan kosong setiap kali pengguna gagal validasi, dan harus mengisi ulang dari awal. Direktif @selected yang kita gunakan untuk dropdown adalah direktif Blade yang ditambahkan di Bab 11 — ia menambahkan atribut selected secara kondisional.
Custom Rule
Terkadang rules bawaan tidak cukup. Misalnya, kita ingin memastikan judul catatan tidak mengandung kata-kata tertentu yang dilarang. Buat custom rule dengan Artisan:
php artisan make:rule JudulBersih
<?php
// app/Rules/JudulBersih.php
namespace App\Rules;
use Closure;
use Illuminate\Contracts\Validation\ValidationRule;
class JudulBersih implements ValidationRule
{
private array $kataLarangan = ['spam', 'iklan', 'promo'];
public function validate(string $attribute, mixed $nilai, Closure $fail): void
{
$judulKecil = strtolower($nilai);
foreach ($this->kataLarangan as $kata) {
if (str_contains($judulKecil, $kata)) {
$fail("Judul tidak boleh mengandung kata '{$kata}'.");
return;
}
}
}
}
Gunakan di Form Request:
use App\Rules\JudulBersih;
public function rules(): array
{
return [
'judul' => ['required', 'string', 'min:3', 'max:200', new JudulBersih()],
'isi' => ['required', 'string', 'min:10'],
];
}
Latihan
Coba kerjakan latihan berikut sebagai pengembangan dari CatatanController yang sudah ada:
-
Form Request untuk update — Buat
PerbaruiCatatanRequestyang memastikan judul tidak duplikat (kecuali milik catatan yang sedang diedit). Methodauthorize()harus memastikan hanya pemilik catatan yang bisa mengedit. -
Rule kondisional — Tambahkan field
selesai_padake form catatan. Field ini hanya wajib diisi jikastatuscatatan adalahselesai, dengan tipe datadateyang tidak boleh di masa depan. Tambahkan validasi yang sesuai di Form Request. -
Custom rule — Buat
MinKataTigarule yang memvalidasi bahwa isi catatan minimal mengandung tiga kata berbeda. Sertakan pesan error yang deskriptif.
Penutup Bab
Validasi di Laravel bukan sekadar pengecekan format — ia adalah lapisan kontrak antara aplikasi dan pengguna. Data yang masuk ke controller method sudah terjamin bersih, dan pengguna mendapat feedback yang jelas saat ada yang keliru. Form Request membuat kontrak itu terdokumentasi dan mudah diuji secara terpisah dari controller.
Semua yang kita bangun sejauh ini — routing, controller, request, response, view Blade, validasi — masih bisa diakses oleh siapa saja yang tahu URL-nya. Catatan yang kita simpan belum punya pemilik, dan siapapun bisa melihat, mengubah, bahkan menghapus data orang lain. Sebelum kita menyentuh database lebih dalam, ada satu lapisan yang harus dipasang terlebih dahulu: autentikasi. Itulah yang akan kita bangun di bab berikutnya — sistem login dan register yang membuat aplikasi tahu siapa yang sedang menggunakannya.