Kelas Form Request adalah permintaan khusus yang memindahkan aturan validasi dan otorisasi dari aksi controller ke kelas tersendiri. Topik ini membahas cara membuat kelas tersebut dengan Artisan, mengisi metode rules() beserta hook otorisasi authorize(), lalu menggunakannya melalui type hint pada parameter aksi controller. Hasil akhirnya adalah validasi yang terorganisir per endpoint, dengan aksi controller yang tetap berfokus pada logika penyimpanan data.
Membuat kelas Form Request#
Form Request adalah kelas turunan (subclass) dari Illuminate\Foundation\Http\FormRequest yang dibuat untuk satu endpoint atau satu kelompok endpoint. Kelas ini dibuat dengan perintah make:request, dan namanya lazim diawali kata kerja yang menyatakan maksud permintaannya, misalnya awalan Store untuk permintaan penyimpanan. Kelas yang dihasilkan diletakkan pada direktori app/Http/Requests sesuai namespace-nya.
Satu kelas melayani satu maksud permintaan, misalnya StoreNoteRequest untuk menyimpan catatan dan UpdateNoteRequest untuk memperbaruinya. Pemisahan ini membuat aturan endpoint penyimpanan boleh berbeda dari endpoint pembaruan tanpa saling mengganggu, sekaligus menjaga setiap berkas tetap kecil dan mudah dicari.
# tidak-dijalankan
# membuat kelas Form Request untuk penyimpanan catatan
cd ~/aplikasi-catatan
php artisan make:request StoreNoteRequestPerintah tersebut tidak dieksekusi ulang pada contoh ini karena kelasnya sudah ada. Keluaran asli dari perintah itu adalah sebagai berikut.
INFO Request [app/Http/Requests/StoreNoteRequest.php] created successfully. Kerangka kelas yang dihasilkan memuat dua metode yang wajib dipahami sebelum digunakan. Metode authorize() pada kerangka bawaan mengembalikan false, sehingga setiap permintaan yang menggunakan kelas ini akan ditolak dengan status 403 sampai metode tersebut disesuaikan. Metode rules() mengembalikan array kosong, yang artinya belum ada aturan apa pun yang diperiksa. Kedua metode inilah yang diisi pada bagian berikutnya.
Menentukan aturan pada rules() dan hook authorize()#
Isi rules() ditulis dengan format yang sama persis dengan array aturan pada validate(), sehingga pengetahuan tentang aturan bawaan dapat langsung dipindahkan. Perbedaannya hanya pada tempatnya: aturan tidak lagi tertulis di dalam aksi controller, melainkan pada kelas yang dapat digunakan ulang oleh beberapa aksi sekaligus.
Metode authorize() dijalankan lebih dahulu, sebelum satu aturan pun diperiksa. Apabila metode ini mengembalikan false, Laravel memotong permintaan dengan exception otorisasi dan merespons 403 tanpa menyentuh rules(). Di dalam metode ini seluruh kemampuan objek permintaan tersedia, termasuk route() untuk membaca parameter rute. Pada contoh aplikasi catatan, saluran arsip diperlakukan sebagai rute hanya-baca, sehingga permintaan penyimpanan ke sana ditolak melalui hook ini.
# menulis kelas StoreNoteRequest yang lengkap
cd ~/aplikasi-catatan
mkdir -p app/Http/Requests
cat > app/Http/Requests/StoreNoteRequest.php <<'PHP'
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class StoreNoteRequest extends FormRequest
{
public function authorize(): bool
{
return $this->route('saluran') !== 'arsip';
}
public function rules(): array
{
return [
'judul' => ['required', 'string', 'max:20'],
'isi' => ['required', 'string'],
];
}
}
PHPPada kelas di atas, authorize() membaca parameter rute bernama saluran dan menolak permintaan ke saluran arsip, sedangkan rules() mengulang aturan yang sama dengan contoh validasi sebelumnya, yaitu judul wajib berupa teks paling banyak 20 karakter dan isi wajib terisi. Seluruh logika pemeriksaan endpoint kini berada di satu berkas yang tidak bergantung pada detail controller-nya.
Menggunakan Form Request pada controller#
Kelas Form Request diaktifkan dengan menuliskannya sebagai type hint pada parameter aksi. Saat permintaan tiba, service container membuat objek kelas tersebut, menjalankan authorize(), kemudian memeriksa rules() sebelum aksi dieksekusi. Aksi hanya menerima permintaan yang lolos keduanya, dan objek yang diterimanya sudah menyediakan metode validated() yang mengembalikan kolom-kolom yang diperiksa.
// tidak-dijalankan
public function store(StoreNoteRequest $request)
{
Note::create($request->validated());
return redirect('/catatan')
->with('sukses', 'Catatan berhasil disimpan.');
}Dibandingkan dengan validate() yang aturannya tertulis pada aksi, bentuk ini membuat controller tidak perlu mengetahui detail aturan sama sekali. Pernyataan use App\Http\Requests\StoreNoteRequest; pada bagian atas berkas controller menjadi bagian penting, karena type hint yang mengacu ke kelas yang tepat adalah pemicu seluruh mekanisme ini.
Skrip uji pada topik ini membuka kembali formulir demo setelah setiap permintaan, sehingga satu view sederhana yang menampilkan daftar error, nilai old(), dan pesan sukses perlu disiapkan pada proyek aplikasi catatan. View ini digunakan bersama rute demo yang dideklarasikan skrip, lengkap dengan direktif @csrf karena permintaannya menyimpan data:
# menyiapkan view formulir demo untuk uji form request
cd ~/aplikasi-catatan
mkdir -p resources/views/uji-form-request
cat > resources/views/uji-form-request/formulir.blade.php <<'BLADE'
@if ($errors->any())
<ul>
@foreach ($errors->all() as $error)
<li>{{ $error }}</li>
@endforeach
</ul>
@endif
<form method="POST" action="/uji/form-request/umum">
@csrf
<label for="judul">Judul</label>
<input id="judul" name="judul" type="text" value="{{ old('judul') }}">
<label for="isi">Isi</label>
<textarea id="isi" name="isi" rows="3">{{ old('isi') }}</textarea>
<button type="submit">Simpan catatan</button>
</form>
@if (session('sukses'))
<p>{{ session('sukses') }}</p>
@endif
BLADEPerilaku lengkapnya diuji dengan skrip berikut, yang mendaftarkan rute demo dengan aksi bertipe StoreNoteRequest lalu mengirim tiga permintaan POST berurutan: data yang melanggar aturan, data yang valid, dan data yang sama dikirim ke saluran arsip. Setiap permintaan gagal diikuti pembukaan formulir dengan session yang sama untuk membaca pesan error dan nilai old().
<?php
use App\Http\Requests\StoreNoteRequest;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
$root = getcwd();
if (! is_file($root.'/bootstrap/app.php')) {
$root = getenv('HOME').'/aplikasi-catatan';
}
require $root.'/vendor/autoload.php';
$app = require $root.'/bootstrap/app.php';
$app->make(Illuminate\Contracts\Console\Kernel::class)->bootstrap();
Route::middleware([
Illuminate\Session\Middleware\StartSession::class,
Illuminate\View\Middleware\ShareErrorsFromSession::class,
])->group(function () {
Route::view('/uji/form-request/formulir', 'uji-form-request.formulir');
Route::post('/uji/form-request/{saluran}', function (StoreNoteRequest $request) {
return redirect('/uji/form-request/formulir')
->with('sukses', 'Catatan tersimpan dengan judul: '.$request->validated('judul'));
});
});
$kernel = $app->make(Illuminate\Contracts\Http\Kernel::class);
function teruskanCookie($respons, Request $request): Request
{
foreach ($respons->headers->getCookies() as $cookie) {
$request->cookies->set($cookie->getName(), $cookie->getValue());
}
return $request;
}
function bukaFormulir($kernel, $responsSebelumnya): string
{
$request = Request::create('http://localhost/uji/form-request/formulir', 'GET');
teruskanCookie($responsSebelumnya, $request);
return $kernel->handle($request)->getContent();
}
function daftarError(string $html): array
{
preg_match_all('/<li>([^<]+)<\/li>/', $html, $cocok);
return $cocok[1];
}
// skenario 1: aturan rules() menolak data pada saluran umum
$resp1 = $kernel->handle(Request::create('http://localhost/uji/form-request/umum', 'POST', [
'judul' => str_repeat('A', 30),
'isi' => '',
], [], [], ['HTTP_REFERER' => 'http://localhost/uji/form-request/formulir']));
echo '1. POST tidak valid: status '.$resp1->getStatusCode().', alihan ke '.$resp1->headers->get('Location').PHP_EOL;
$html1 = bukaFormulir($kernel, $resp1);
foreach (daftarError($html1) as $i => $pesan) {
echo ' error '.($i + 1).': '.$pesan.PHP_EOL;
}
preg_match('/name="judul" type="text" value="([^"]*)"/', $html1, $judul1);
echo ' old judul: '.($judul1[1] === '' ? '(kosong)' : 'terisi ulang').PHP_EOL;
// skenario 2: data valid pada saluran umum
$resp2 = $kernel->handle(Request::create('http://localhost/uji/form-request/umum', 'POST', [
'judul' => 'Rapat pekanan',
'isi' => 'Mencatat keputusan rapat.',
], [], [], ['HTTP_REFERER' => 'http://localhost/uji/form-request/formulir']));
echo '2. POST data valid: status '.$resp2->getStatusCode().PHP_EOL;
$html2 = bukaFormulir($kernel, $resp2);
echo ' pesan sukses: '.(str_contains($html2, 'Catatan tersimpan dengan judul: Rapat pekanan') ? 'terbaca' : 'tidak ada').PHP_EOL;
// skenario 3: hook authorize() menolak saluran arsip
$resp3 = $kernel->handle(Request::create('http://localhost/uji/form-request/arsip', 'POST', [
'judul' => 'Rapat pekanan',
'isi' => 'Mencatat keputusan rapat.',
], [], [], ['HTTP_REFERER' => 'http://localhost/uji/form-request/formulir']));
echo '3. POST ke saluran arsip: status '.$resp3->getStatusCode().PHP_EOL;
$html3 = bukaFormulir($kernel, $resp3);
echo ' pesan sukses: '.(str_contains($html3, 'Catatan tersimpan') ? 'terbaca' : 'tidak ada').PHP_EOL;1. POST tidak valid: status 302, alihan ke http://localhost/uji/form-request/formulir
error 1: The judul field must not be greater than 20 characters.
error 2: The isi field is required.
old judul: terisi ulang
2. POST data valid: status 302
pesan sukses: terbaca
3. POST ke saluran arsip: status 403
pesan sukses: tidak adaSkenario pertama memperlihatkan rules() bekerja persis seperti validate(), lengkap dengan respons 302, dua pesan error, dan masukan lama yang tersedia bagi old(). Skenario kedua lolos seluruh pemeriksaan sehingga validated('judul') terbaca pada pesan sukses. Skenario ketiga memperlihatkan hook authorize() bekerja sebelum validasi: data yang sama yang tadinya lolos kini ditolak dengan status 403, dan tidak ada pesan sukses yang tertulis.
Kesalahan umum#
Kesalahan pertama adalah membiarkan authorize() bawaan mengembalikan false. Setiap permintaan yang menggunakan kelasnya langsung dijawab 403 meskipun datanya sempurna, sehingga terkesan validasinya bermasalah padahal pemeriksaan belum sampai ke rules(). Ciri khasnya adalah status 403 yang muncul sebelum formulir sempat menampilkan satu pesan error validasi pun. Ubah metode ini menjadi true apabila belum ada aturan otorisasi khusus, atau isi dengan logika otorisasinya.
Kesalahan kedua adalah mengambil data dengan $request->all() alih-alih validated(). Array hasil all() memuat seluruh kolom yang dikirim, termasuk kolom yang tidak dikenal aturannya, sehingga membuka peluang mass assignment pada model yang $fillable-nya longgar. Gunakan validated() agar hanya kolom yang diperiksa yang ikut disimpan.
Kesalahan ketiga adalah salah mengimpor kelas pada controller. Type hint yang mengarah ke Illuminate\Http\Request alih-alih App\Http\Requests\StoreNoteRequest membuat mekanisme Form Request tidak pernah berjalan, dan aksi memproses data tanpa validasi apa pun. Ciri khasnya adalah aksi tetap berjalan untuk data kosong sekalipun, karena yang dioperkan hanyalah Request biasa tanpa aturan. Periksa pernyataan use di bagian atas berkas apabila validasi terasa tidak bekerja.
Ringkasan#
- Form Request memindahkan aturan validasi ke metode
rules()dan otorisasi ke hookauthorize()pada satu kelas per endpoint. - Metode
authorize()dijalankan sebelum validasi, dan nilaifalsememotong permintaan dengan respons 403. - Aksi controller cukup melakukan type hint terhadap kelasnya, lalu mengambil data yang diperiksa melalui
validated(). - Kelas yang sama dapat digunakan ulang di beberapa aksi sehingga aturan tetap berasal dari satu sumber.
Referensi#
- Validation — Laravel — Dokumentasi Form Request, metode authorize() dan rules(), serta pemakaiannya pada controller.
- Controllers — Laravel — Dokumentasi aksi controller dan injeksi dependensi pada parameter aksinya.