Query string di chapter sebelumnya cocok untuk data opsional seperti nomor halaman, tetapi data yang dikirim pengguna — pendaftaran, pembuatan tugas, unggahan dokumen — dikirim lewat form HTML dengan HTTP method POST. Kita sudah mencicipi pembacaannya lewat $request->input() pada chapter Request Object
; sekarang kita rakit alur lengkapnya: form Blade yang merender HTML, route yang menerimanya, upload file, sampai pola old input yang mengembalikan isian pengguna ketika penyimpanan gagal. Validasi yang teliti sengaja kita tangguhkan — Laravel punya bagian khusus untuk itu di section selanjutnya.
Form HTML dan Route Tujuan #
Form kita taruh di view resources/views/tugas/create.blade.php. Atribut method="POST" menentukan HTTP method yang dipakai browser, sedangkan action menunjuk route bernama tugas.simpan yang akan kita daftarkan bersamanya:
<form method="POST" action="{{ route('tugas.simpan') }}">
@csrf
<label for="nama">Nama tugas</label>
<input type="text" id="nama" name="nama" value="{{ old('nama') }}">
<label for="deskripsi">Deskripsi</label>
<textarea id="deskripsi" name="deskripsi">{{ old('deskripsi') }}</textarea>
<button type="submit">Simpan Tugas</button>
</form>Directive @csrf adalah bagian penting yang tidak boleh dilupakan: directive tersebut merender hidden input _token berisi token acak dari session. Route web dilindungi middleware anti-request-forgery yang membandingkan _token dengan session — token yang hilang atau tidak cocok membuat request dijawab error 419 Page Expired. Karena nilainya acak setiap session, kita sembunyikan isinya saat menampilkan hasil render nyata berikut:
Output yang Dihasilkan:
<form method="POST" action="http://localhost:8000/tugas">
<input type="hidden" name="_token" value="(token acak)" autocomplete="off">
<label for="nama">Nama tugas</label>
<input type="text" id="nama" name="nama" value="">
<label for="deskripsi">Deskripsi</label>
<textarea id="deskripsi" name="deskripsi"></textarea>
<button type="submit">Simpan Tugas</button>
</form>Dua route di belakangnya — satu untuk menampilkan form, satu untuk menerima kiriman:
<?php
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
Route::get('/tugas/baru', function () {
return view('tugas.create');
})->name('tugas.baru');
Route::post('/tugas', function (Request $request) {
if (! $request->filled('nama')) {
return redirect()->route('tugas.baru')->withInput();
}
return 'Tugas "'.$request->input('nama').'" disimpan.';
})->name('tugas.simpan');Handler POST /tugas sementara ini hanya memeriksa filled() — pemeriksaan minimal agar kita bisa fokus pada alur form, bukan aturan validasi.
Membaca Data Form di Sisi Server #
Saat tombol Simpan Tugas ditekan, browser mengirim body request berisi pasangan nama=...&deskripsi=.... Karena request POST melewati pemeriksaan CSRF, kita uji lewat feature test yang menonaktifkannya otomatis:
<?php
// tests/Feature/FormDataTest.php (potongan)
$response = $this->post('/tugas', [
'nama' => 'Belajar Form Laravel',
'deskripsi' => 'Latihan mengirim form',
]);
dump($response->getContent());Output yang Dihasilkan:
"Tugas "Belajar Form Laravel" disimpan."Array kedua pada $this->post() menjadi body request, dan setiap field-nya terbaca di handler lewat $request->input('nama') — perilaku yang sudah kita kenal, karena input() membaca sumber body POST.
Upload File dengan multipart #
Form biasa tidak cukup untuk file; atribut enctype="multipart/form-data" wajib ditambahkan pada tag <form>, dan handler membacanya lewat $request->file():
use Illuminate\Support\Facades\Storage;
Route::post('/tugas/upload', function (Request $request) {
$file = $request->file('lampiran');
$path = $file->store('lampiran');
return [
'nama_asli' => $file->getClientOriginalName(),
'ekstensi' => $file->extension(),
'ukuran' => $file->getSize(),
'folder_tersimpan' => dirname($path),
'tersimpan' => Storage::exists($path),
];
})->name('tugas.upload');Method store('lampiran') langsung menyimpan file ke disk default dengan nama unik berbasis hash — itulah kenapa kita menampilkan dirname($path) dan status Storage::exists() alih-alih path penuhnya, karena nama hash-nya berubah di setiap eksekusi. File dokumen.pdf sebesar 100 KB kita kirim dari feature test memakai file palsu bawaan Laravel:
Storage::fake();
$response = $this->post('/tugas/upload', [
'lampiran' => UploadedFile::fake()->create('dokumen.pdf', 100),
]);
dump($response->json());Output yang Dihasilkan:
array:5 [
"nama_asli" => "dokumen.pdf"
"ekstensi" => "pdf"
"ukuran" => 102400
"folder_tersimpan" => "lampiran"
"tersimpan" => true
]Old Input saat Penyimpanan Gagal #
Sekarang kita saksikan cabang gagal pada handler. Ketika nama kosong, handler tidak menampilkan error — handler mengarahkan pengguna kembali ke form sambil membawa withInput(), yang menyimpan seluruh input saat itu ke session sebagai flash data:
public function test_old_input_setelah_penyimpanan_gagal(): void
{
$response = $this->post('/tugas', [
'nama' => '',
'deskripsi' => 'Catatan lama',
]);
dump($response->status());
dump($response->headers->get('Location'));
$response = $this->get('/tugas/baru');
// nilai _token acak disamarkan
echo preg_replace(
'/name="_token" value="[^"]+"/',
'name="_token" value="(token acak)"',
$response->getContent()
);
}Output yang Dihasilkan:
302
"http://localhost:8000/tugas/baru"
<form method="POST" action="http://localhost:8000/tugas">
<input type="hidden" name="_token" value="(token acak)" autocomplete="off">
<label for="nama">Nama tugas</label>
<input type="text" id="nama" name="nama" value="">
<label for="deskripsi">Deskripsi</label>
<textarea id="deskripsi" name="deskripsi">Catatan lama</textarea>
<button type="submit">Simpan Tugas</button>
</form>Response pertama berstatus 302 dengan header Location menunjuk form. Pada render kedua, {{ old('deskripsi') }} di Blade terisi 'Catatan lama' — flash data dari request sebelumnya hidup tepat satu request berikutnya, sesuai mekanisme old input pada dokumentasi requests
. Perhatikan pula bahwa value="" pada nama tetap kosong: string kosong sudah diubah middleware ConvertEmptyStringsToNull menjadi null sebelum di-flash, persis perilaku yang kita jumpai di chapter Request Object
. Detail penggunaan old() bersama validasi dan @error akan dibahas pada chapter validasi.
Alur form kini lengkap dari sisi request: render, kirim, simpan atau kembali dengan isian lama. Tetapi browser dan client API tidak hanya mengirim data — mereka juga mengirim header seperti Accept dan Authorization yang menentukan format response dan identitas pemanggil. Chapter berikutnya mengajak kita membaca header-header tersebut lewat object Request.