Form adalah bagian aplikasi yang paling sering berinteraksi dengan pengguna — dan paling mudah membuat mereka frustrasi. Skenario buruknya klasik: pengguna mengisi dua puluh kolom, satu kolom lupa diisi, validasi menolak, halaman di-refresh, dan seluruh isian lenyap. Laravel mencegahnya dengan tiga alat yang saling melengkapi: @csrf menjaga keamanan form, helper old() mengisi ulang nilai dari request sebelumnya, dan variabel $errors membawa pesan validasi ke tampilan. Chapter ini membangun form lengkap — input teks, textarea, select, radio, checkbox — lalu membuktikan lewat feature test bahwa data benar-benar mengalir kembali melewati redirect.

Kerangka Route dengan Validasi #

Form membutuhkan dua route: satu menampilkan form, satu memproses submit. Method validate() pada object $request menjalankan aturan validasi dan otomatis redirect kembali ke form beserta error dan old input bila ada yang gagal:

php
<?php

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;

Route::get('/tugas/baru', fn () => view('tugas.form'))->name('tugas.form');

Route::post('/tugas', function (Request $request) {
    $data = $request->validate([
        'judul' => 'required|min:3',
        'deskripsi' => 'nullable|max:200',
        'prioritas' => 'required|in:rendah,sedang,tinggi',
        'kategori' => 'required|in:kerja,rumah',
        'selesai' => 'boolean',
    ]);

    return redirect()
        ->route('tugas.sukses')
        ->with('pesan', 'Tugas "'.$data['judul'].'" tersimpan.');
})->name('tugas.simpan');

Route::get('/tugas/sukses', fn () => 'Tugas tersimpan.')->name('tugas.sukses');

Saat validasi gagal, validate() melempar ValidationException yang berubah menjadi redirect 302 ke halaman sebelumnya. Laravel menyimpan seluruh input ke session flash old sekaligus pesan error ke errors — keduanya otomatis tersedia di view tanpa kita mengirim apa pun dari controller.

Form Lengkap dengan @csrf dan old() #

Setiap jenis input punya cara mengisi ulang yang berbeda, dan Blade menyediakan directive khusus untuk kondisi terpilih. Berkas resources/views/tugas/form.blade.php:

blade
<form method="POST" action="{{ route('tugas.simpan') }}">
    @csrf

    @if ($errors->any())
        <div class="ringkasan-error">
            <strong>Periksa kembali isian Anda:</strong>
            <ul>
                @foreach ($errors->all() as $pesan)
                    <li>{{ $pesan }}</li>
                @endforeach
            </ul>
        </div>
    @endif

    <label for="judul">Judul</label>
    <input id="judul" type="text" name="judul" value="{{ old('judul') }}">
    @error('judul')
        <p class="field-error">{{ $message }}</p>
    @enderror

    <label for="deskripsi">Deskripsi</label>
    <textarea id="deskripsi" name="deskripsi">{{ old('deskripsi') }}</textarea>

    <label for="prioritas">Prioritas</label>
    <select id="prioritas" name="prioritas">
        @foreach (['rendah' => 'Rendah', 'sedang' => 'Sedang', 'tinggi' => 'Tinggi'] as $nilai => $teks)
            <option value="{{ $nilai }}" @selected(old('prioritas') === $nilai)>{{ $teks }}</option>
        @endforeach
    </select>

    <fieldset>
        <legend>Kategori</legend>
        @foreach (['kerja' => 'Pekerjaan', 'rumah' => 'Rumah'] as $nilai => $teks)
            <label>
                <input type="radio" name="kategori" value="{{ $nilai }}" @checked(old('kategori') === $nilai)>
                {{ $teks }}
            </label>
        @endforeach
    </fieldset>

    <label>
        <input type="checkbox" name="selesai" value="1" @checked(old('selesai'))>
        Sudah selesai
    </label>

    <button type="submit">Simpan</button>
</form>

Empat pola pengisian ulang bekerja di sini. Input teks memakai value="{{ old('judul') }}". Isi textarea diletakkan di antara tag pembuka dan penutup. Option pada select memakai directive @selected yang merender atribut selected saat kondisinya benar. Checkbox dan radio memakai @checked — checkbox selesai cukup @checked(old('selesai')) karena nilai '1' dari checkbox bernilai truthy.

Dua kebiasaan kecil membuat pola-pola itu konsisten. Helper old() menerima argumen kedua sebagai nilai default saat tidak ada input lama — old('prioritas', 'sedang') mengembalikan sedang saat halaman dibuka biasa, sehingga form edit dan form kosong bisa memakai component yang sama. Untuk textarea, isian harus diletakkan sebagai konten elemen, bukan di atribut value — spesifikasi HTML memang tidak mengenal atribut value pada textarea, jadi old('deskripsi') yang ditempel di atribut tidak akan pernah tampil. Daftar lengkap aturan validasi yang bisa dipasang pada validate() tersedia di dokumentasi Validation .

Hasil Render Setelah Validasi Gagal #

Kita submit form dengan judul = 'AB' — hanya dua karakter, padahal aturan menuntut minimal tiga. Laravel redirect kembali ke form, dan inilah HTML yang dikirim ke browser (nilai _token disamarkan karena acak per sesi; URL action mengikuti konfigurasi APP_URL):

text
<form method="POST" action="http://localhost:8000/tugas">
    <input type="hidden" name="_token" value="(token sesi acak)" autocomplete="off">
            <div class="ringkasan-error">
            <strong>Periksa kembali isian Anda:</strong>
            <ul>
                                    <li>The judul field must be at least 3 characters.</li>
                            </ul>
        </div>

    <label for="judul">Judul</label>
    <input id="judul" type="text" name="judul" value="AB">
            <p class="field-error">The judul field must be at least 3 characters.</p>

    <label for="deskripsi">Deskripsi</label>
    <textarea id="deskripsi" name="deskripsi">Belajar form dan old input.</textarea>

    <label for="prioritas">Prioritas</label>
    <select id="prioritas" name="prioritas">
                    <option value="rendah" >Rendah</option>
                    <option value="sedang" >Sedang</option>
                    <option value="tinggi" selected>Tinggi</option>
            </select>

    <fieldset>
        <legend>Kategori</legend>
                    <label>
                <input type="radio" name="kategori" value="kerja" >
                Pekerjaan
            </label>
                    <label>
                <input type="radio" name="kategori" value="rumah" checked>
                Rumah
            </label>
            </fieldset>

    <label>
        <input type="checkbox" name="selesai" value="1" checked>
        Sudah selesai
    </label>

    <button type="submit">Simpan</button>
</form>

Semua isian kembali: value="AB" pada input judul, teks deskripsi di dalam textarea, selected pada option tinggi, checked pada radio rumah, dan checked pada checkbox. Pesan error tampil dua kali — di ringkasan atas dan di bawah kolom judul. Perilaku ini semua dihasilkan dua inputan request yang berbeda: request POST yang membawa data dan request GET hasil redirect yang merender ulang form, dengan session sebagai jembatan keduanya.

Menguji Alur Old Input dengan Feature Test #

Feature test membuktikan alurnya ujung ke ujung. Method followingRedirects() mengikuti redirect 302 sampai ke halaman form yang dirender ulang, lalu assertion memeriksa isiannya:

php
public function test_validasi_gagal_dan_old_input_mengalir_lewat_redirect(): void
{
    $this->get(route('tugas.form')); // mengatur halaman asal redirect back

    $response = $this->followingRedirects()->post(route('tugas.simpan'), [
        'judul' => 'AB',
        'deskripsi' => 'Belajar form dan old input.',
        'prioritas' => 'tinggi',
        'kategori' => 'rumah',
        'selesai' => '1',
    ]);

    $response->assertStatus(200);
    $response->assertSee('value="AB"', false);
    $response->assertSee('Belajar form dan old input.', false);
    $response->assertSeeInOrder(['value="tinggi"', 'selected'], false);
    $response->assertSee('The judul field must be at least 3 characters.');
}

Output yang Dihasilkan untuk seluruh berkas test — termasuk test bahwa old() tersimpan sebagai session flash dan test submit sukses:

text
PASS  Tests\Feature\TugasFormTest
✓ validasi gagal dan old input mengalir lewat redirect              0.15s
✓ old input tersimpan sebagai session flash                          0.01s
✓ validasi berhasil meredirect dengan pesan                          0.01s

Tests:    3 passed (12 assertions)
Duration: 0.24s

Old input tersimpan sebagai session flash, artinya hanya bertahan satu request — setelah pengguna memperbaiki isian dan berhasil, redirect ke halaman sukses otomatis mengosongkannya. Satu potongan di form ini belum kita bedah perannya: input _token tersembunyi dari @csrf. Fungsi vitalnya — dan apa yang terjadi bila ia hilang atau tidak cocok — menjadi fokus chapter berikutnya.