Hari ini saya masuk ke materi yang langsung terasa “nyambung” dengan kerjaan sehari-hari: form handling dan validasi. Dari semua bagian Laravel yang pernah saya coba, ini yang paling sering saya pakai — karena hampir semua aplikasi web pasti menerima input dari user, entah itu form login, form pendaftaran, atau form tambah data. Dan kalau input itu tidak dijaga, aplikasi bisa jebol. Jadi catatan kali ini saya buat agak lengkap, terutama soal CSRF dan Form Request yang sempat bikin saya menepuk jidat: “oh, selama ini saya repot-repet manual terus!”
Kenapa form di Laravel harus pakai @csrf?
Waktu pertama kali bikin form di Laravel, saya lupa menaruh @csrf dan dapat error 419 Page Expired. Saya sempat bingung, padahal form saya sudah benar. Ternyata ini justru fitur keamanan: Laravel melindungi aplikasi dari serangan CSRF (Cross-Site Request Forgery) — yaitu upaya pihak lain mengirim request atas nama user yang sedang login tanpa sepengetahuannya.
Cara kerjanya: Laravel membangkitkan token acak per sesi, disimpan di session, lalu form kita kirim token itu lewat field tersembunyi. Saat request masuk, Laravel mencocokkan token dari form dengan token di session. Tidak cocok → ditolak dengan 419. Jadi @csrf bukan pelengkap, melainkan wajib untuk semua form dengan method POST (PUT, PATCH, DELETE memakainya lewat method spoofing).
<form method="POST" action="/tasks">
@csrf
<input type="text" name="title">
<button type="submit">Simpan</button>
</form>Saya pernah bertanya-tanya: bagaimana kalau ada route yang memang tidak boleh di-CSRF-check, misalnya webhook dari pihak ketiga? Laravel menyediakan pengecualian di app/Http/Middleware/ValidateCsrfToken.php. Di sana ada properti $except yang bisa diisi path tertentu:
<?php
// app/Http/Middleware/ValidateCsrfToken.php
protected $except = [
'payment/callback',
'webhooks/*', // wildcard untuk semua sub-path
];Catatan penting: hanya exclude route yang memang benar-benar tidak bisa mengirim token CSRF (misalnya payment gateway). Jangan pernah exclude route yang bisa diakses dari browser biasa — itu sama saja membuka pintu serangan CSRF.
Selain @csrf di Blade, token juga bisa diakses langsung dari PHP dengan csrf_token(), atau melalui meta tag untuk keperluan AJAX:
<meta name="csrf-token" content="{{ csrf_token() }}">
<script>
fetch('/api/tasks', {
method: 'POST',
headers: {
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content,
'Content-Type': 'application/json'
},
body: JSON.stringify({ title: 'Belajar Laravel' })
});
</script>Validasi: jangan percaya input user
Prinsip yang terus saya tanamkan: input dari user itu data asing, jangan pernah dipercaya mentah-mentah. Laravel menyediakan lapisan validasi yang sangat enak dipakai. Cara paling sederhana — dan yang dulu selalu saya pakai — adalah memvalidasi langsung di controller:
<?php
// app/Http/Controllers/TaskController.php
public function store(Request $request)
{
$validated = $request->validate([
'title' => ['required', 'string', 'max:255'],
'due_date' => ['nullable', 'date', 'after_or_equal:today'],
]);
Task::create($validated);
return redirect()->route('tasks.index');
}Kalau validasi gagal, Laravel otomatis mengarahkan kembali ke halaman sebelumnya sambil membawa error di session. Di Blade, error itu tinggal ditampilkan dengan directive @error, dan input lama bisa diisi ulang dengan helper old() supaya user tidak mengetik ulang dari nol:
<input type="text" name="title" value="{{ old('title') }}">
@error('title')
<div class="alert alert-danger">{{ $message }}</div>
@enderrorCheatsheet: aturan validasi yang paling sering dipakai
Saya merangkum aturan validasi yang paling sering saya pakai dalam tabel ini. Daripada bolak-balik buka dokumentasi, lebih efisien kalau punya referensi singkat:
| Aturan | Fungsi | Contoh |
|---|---|---|
required | Wajib diisi | 'name' => 'required' |
email | Harus format email valid | 'email' => 'required|email' |
numeric | Harus angka | 'price' => 'numeric' |
date | Harus format tanggal | 'born' => 'date' |
string | Harus berupa string | 'name' => 'string' |
max:255 | Maksimal 255 karakter | 'title' => 'max:255' |
min:8 | Minimal 8 karakter | 'password' => 'min:8' |
regex:/pattern/ | Cocok dengan regex | 'phone' => 'regex:/^[0-9]+$/ ' |
unique:users | Nilai unik di tabel | 'email' => 'unique:users' |
confirmed | Harus cocok dengan field *_confirmation | 'password' => 'confirmed' |
in:a,b,c | Harus salah satu dari opsi | 'status' => 'in:active,inactive' |
nullable | Boleh kosong | 'bio' => 'nullable|string' |
Pesan error custom
Pesan bawaan Laravel sudah cukup jelas, tapi kadang saya butuh pesan yang lebih spesifik. Ada dua cara: langsung di array messages() atau lewat file bahasa. Cara pertama — paling cepat — cukup tambahkan parameter kedua di validate():
<?php
$request->validate(
[
'email' => ['required', 'email', 'unique:users'],
'password' => ['required', 'min:8'],
],
[
'email.required' => 'Email wajib diisi.',
'email.unique' => 'Email sudah terdaftar.',
'password.min' => 'Password minimal 8 karakter.',
]
);Cara kedua, kalau pesan itu akan dipakai di banyak tempat, buat file lang/id/validation.php (atau lang/en/validation.php) dan override attribute yang ingin diubah. Misalnya, mengganti field name email menjadi “alamat email” di semua pesan error tanpa menulis ulang satu per satu:
<?php
// lang/id/validation.php (excerpt)
'attributes' => [
'email' => 'alamat email',
'password' => 'kata sandi',
'title' => 'judul',
],Custom validation rule dengan class
Kadang aturan bawaan tidak cukup. Misalnya, saya ingin memastikan string hanya boleh berisi huruf tanpa angka. Laravel memungkinkan membuat custom rule sebagai class:
php artisan make:rule NoNumbers
<?php
// app/Rules/NoNumbers.php
namespace App\Rules;
use Closure;
use Illuminate\Contracts\Validation\ValidationRule;
class NoNumbers implements ValidationRule
{
public function validate(string $attribute, mixed $value, Closure $fail): void
{
if (preg_match('/[0-9]/', $value)) {
$fail("{{attribute}} tidak boleh mengandung angka.");
}
}
}Penggunaannya tinggal masukkan ke array rules seperti aturan lainnya:
<?php
use App\Rules\NoNumbers;
$request->validate([
'username' => ['required', 'string', 'max:50', new NoNumbers],
]);Form Request: validasi dipindah dari controller
Begitu controller mulai penuh aturan validasi, saya baru sadar ada cara yang lebih rapi: Form Request. Ini class khusus yang berisi aturan validasi, jadi controller tetap ramping dan aturan bisa dipakai ulang. Membuatnya semudah menjalankan artisan:
php artisan make:request StoreTaskRequest
<?php
// app/Http/Requests/StoreTaskRequest.php
class StoreTaskRequest extends FormRequest
{
public function authorize(): bool
{
return true; // false = 403, dipakai untuk cek otorisasi
}
public function rules(): array
{
return [
'title' => ['required', 'string', 'max:255'],
'due_date' => ['nullable', 'date', 'after_or_equal:today'],
];
}
}Lalu di controller, tipe parameternya tinggal diganti dari Request menjadi StoreTaskRequest. Validasi jalan otomatis sebelum method dieksekusi — kalau gagal, user dikembalikan ke form beserta error-nya. Ini contoh nyata bagaimana Laravel mendorong kode yang rapi sebagai kebiasaan, bukan sebagai aturan yang dipaksakan.
Menambahkan custom messages di Form Request
Form Request juga mendukung method messages() untuk pesan error custom. Saya sering pakai ini supaya pesan error lebih ramah pengguna:
<?php
// Di dalam StoreTaskRequest
public function messages(): array
{
return [
'title.required' => 'Judul task wajib diisi.',
'title.max' => 'Judul terlalu panjang, maksimal 255 karakter.',
'due_date.date' => 'Format tanggal tidak valid.',
];
}Method after(): hook setelah validasi berhasil
Fitur yang baru saya sadari belakangan: after() method di FormRequest. Method ini dijalankan setelah aturan utama validasi lolos — cocok untuk cross-field validation yang kompleks, misalnya memastikan tanggal end lebih besar dari tanggal start:
<?php
use Illuminate\Validation-rule;
public function withValidator($validator): void
{
$validator->after(function ($validator) {
if ($this->start_date && $this->end_date) {
if ($this->end_date < $this->start_date) {
$validator->errors()->add(
'end_date',
'Tanggal selesai harus setelah tanggal mulai.'
);
}
}
});
}File upload validation
Form yang menerima upload file membutuhkan validasi ekstra. Bukan hanya soal format, tapi juga ukuran dan tipe MIME. Laravel menyediakan aturan khusus untuk ini:
<?php
$request->validate([
'avatar' => ['required', 'file', 'image', 'mimes:jpeg,png,webp', 'max:2048'],
// max:2048 = maksimal 2 MB (dalam KB)
]);Setelah lolos validasi, file dipindah ke storage dengan store():
<?php
$path = $request->file('avatar')->store('avatars', 'public');
// Tersimpan di storage/app/public/avatars/xxxxx.jpgSaya sempat terjebak pada satu masalah: upload gagal tanpa error. Ternyata karena php.ini punya batas upload_max_filesize yang lebih kecil dari ukuran file. Jadi pastikan juga konfigurasi PHP-nya mendukung.
Real-time validation dengan AJAX
Supaya user tidak perlu menunggu submit untuk mengetahui error, validasi real-time via AJAX sangat membantu. Di backend, kita buat route khusus yang hanya menerima AJAX:
<?php
// routes/web.php
Route::post('/validate/register', function (Request $request) {
$validator = validator($request->only('email', 'password'), [
'email' => 'required|email|unique:users',
'password' => 'required|min:8|confirmed',
]);
if ($validator->fails()) {
return response()->json(['errors' => $validator->errors()], 422);
}
return response()->json(['valid' => true]);
});Di frontend, setiap kali user selesai mengetik (debounce 300ms), kirim request ke route tersebut. Kalau ada error, tampilkan di bawah field. Kalau valid, tampilkan centang hijau. UX-nya terasa jauh lebih responsif.
Yang saya catat dari episode ini
@csrfwajib di semua form POST — error 419 adalah tanda aman, bukan bug.- CSRF token juga bisa diakses via
csrf_token()atau meta tag untuk AJAX. - Exclude CSRF hanya untuk webhook eksternal yang benar-benar tidak bisa kirim token.
- Validasi inline di controller cukup untuk form sederhana, tapi cepat berantakan.
- Form Request = validasi terpusat + bisa dipakai ulang +
authorize()gratis untuk urusan otorisasi. messages()dan filelang/untuk pesan error yang lebih manusiawi.- Custom rule class memudahkan validasi lintas field atau logika bisnis spesifik.
after()method di FormRequest cocok untuk cross-field validation kompleks.- Upload file: validasi tipe, ukuran, dan MIME. Cek juga
php.ini. - Real-time AJAX validation meningkatkan UX secara signifikan.
old()dan@errormembuat pengalaman user tetap nyaman saat validasi gagal.
Satu momen yang cukup membuka mata: saya dulu berpikir validasi di frontend sudah cukup. Tapi ternyata client-side validation bisa di-bypass dengan mudah — cukup nonaktifkan JavaScript atau kirim request langsung lewat curl. Validasi di server adalah garis pertahanan terakhir yang tidak bisa diabaikan. Client-side itu soal UX, server-side itu soal keamanan. Dua hal berbeda, keduanya penting.
Di episode berikutnya saya mau masuk ke middleware — lapisan yang mengecek request sebelum sampai ke controller. Sepertinya di situlah letak “otak” dari proteksi auth dan CSRF tadi.
Komentar Terbaru