Validasi Form Request: Lebih dari Sekadar required

{"title":"Validasi Form Request: Lebih dari Sekadar required","slug":"validasi-form-request-lebih-dari-sekadar-required","excerpt":"Belajar pola FormRequest Laravel yang scalable: aturan kondisional, pesan custom yang manusiawi, dan validasi lintas field lewat withValidator.","tags":["laravel","validasi","best-practices"],"content":"\n

Kenapa FormRequest Sering Berhenti di required Saja

\n

Hampir semua tutorial Laravel yang saya lihat berhenti mengajarkan FormRequest sebatas 'nama' => 'required|string|max:255'. Itu bagus untuk permulaan, tapi begitu aplikasi bertumbuh — apalagi kalau Anda pernah kerja di sistem yang datanya saling bergantung seperti data pasien, data transaksi, atau data konfigurasi klinik — validasi sesederhana itu cepat kewalahan. Saya sendiri banyak berurusan dengan form yang field-nya wajib tergantung pilihan field lain, atau dua tanggal yang harus konsisten satu sama lain. Kalau logika ini dipaksakan ke controller, controller cepat gemuk dan sulit ditest. FormRequest sebenarnya dirancang untuk menampung semua kerumitan ini, bukan cuma jadi tempat menaruh daftar required.

\n\n

Aturan Kondisional: Validasi yang Mengikuti Konteks

\n

Kasus paling umum: sebuah field baru wajib diisi kalau field lain bernilai tertentu. Laravel punya beberapa cara elegan untuk ini tanpa menulis if-else manual di method rules().

\n
public function rules(): array\n{\n    return [\n        'tipe_pembayaran' => 'required|in:tunai,transfer',\n        'nomor_rekening' => 'required_if:tipe_pembayaran,transfer',\n        'jumlah' => 'required|numeric|min:0',\n        'diskon' => 'sometimes|numeric|lt:jumlah',\n    ];\n}
\n

required_if, required_unless, required_with, dan exclude_if menyelesaikan sebagian besar kasus kondisional sederhana. Untuk logika yang lebih rumit — misalnya \"field ini wajib hanya kalau user login sebagai admin dan tanggal sudah lewat\" — saya lebih suka pakai $validator->sometimes() untuk menambahkan aturan secara kondisional saat runtime, atau menyusun logikanya lewat closure after() di method withValidator(). Untuk kondisi yang bergantung pada closure sederhana, Rule::requiredIf() juga sangat membantu. Cara-cara ini menjaga rules() tetap mudah dibaca sekaligus memberi ruang untuk logika bisnis yang lebih kontekstual.

\n
use Illuminate\\Validation\\Rule;\n\n'jadwal_tindak_lanjut' => [\n    Rule::requiredIf(fn () => $this->user()->isAdmin() && $this->status === 'ditutup'),\n    'date',\n],
\n\n

Pesan Custom yang Benar-Benar Membantu User

\n

Pesan error default Laravel sudah cukup baik, tapi untuk aplikasi yang dipakai orang non-teknis — misalnya petugas administrasi di fasilitas kesehatan — pesan generik seperti \"The nomor rekening field is required when tipe pembayaran is transfer\" sering membingungkan. Method messages() dan attributes() ada untuk ini.

\n
public function messages(): array\n{\n    return [\n        'nomor_rekening.required_if' => 'Nomor rekening wajib diisi jika pembayaran via transfer.',\n        'diskon.lt' => 'Diskon tidak boleh lebih besar dari jumlah tagihan.',\n    ];\n}\n\npublic function attributes(): array\n{\n    return [\n        'nomor_rekening' => 'nomor rekening',\n    ];\n}
\n

Sebagai ilustrasi, pesan yang spesifik seperti ini biasanya membantu mengurangi pertanyaan \"kenapa saya tidak bisa submit form\" dari pengguna — meskipun tentu saja dampaknya berbeda-beda tergantung aplikasi dan penggunanya, dan ini bukan angka yang saya ukur secara formal.

\n\n

Validasi Lintas Field lewat withValidator

\n

Aturan bawaan Laravel bagus untuk membandingkan dua field sederhana (gt, lt, same, different), tapi begitu logikanya melibatkan tiga field atau lebih, atau butuh query ke database, saya menaruhnya di withValidator() lewat closure after().

\n
public function withValidator(Validator $validator): void\n{\n    $validator->after(function ($validator) {\n        if ($this->tanggal_mulai && $this->tanggal_selesai\n            && $this->tanggal_mulai > $this->tanggal_selesai) {\n            $validator->errors()->add(\n                'tanggal_selesai',\n                'Tanggal selesai tidak boleh sebelum tanggal mulai.'\n            );\n        }\n    });\n}
\n

Pendekatan ini juga tempat yang pas untuk validasi yang butuh cek ke database, misalnya memastikan kombinasi dua field unik secara bersamaan — sesuatu yang tidak bisa ditangani rule unique standar kalau kombinasinya melibatkan kolom yang di-derive, bukan langsung dari input.

\n

Kalau logika after() mulai panjang, saya biasanya memecahnya jadi beberapa Custom Rule Object (php artisan make:rule) supaya tiap aturan bisa ditest terpisah dan dipakai ulang di FormRequest lain.

\n\n

Takeaway

\n

FormRequest bukan sekadar tempat menaruh daftar aturan — ia adalah lapisan tempat logika validasi bisnis Anda hidup, terpisah rapi dari controller. Manfaatkan required_if/Rule::requiredIf() untuk kondisional, messages()/attributes() untuk pesan yang manusiawi, dan withValidator() atau Custom Rule Object untuk validasi lintas field yang lebih kompleks. Begitu pola ini terbentuk, menambah aturan baru jadi jauh lebih aman karena semuanya sudah terisolasi dan mudah ditest.

\n