Budi baru saja menyimpan artikel lewat form. Jika controller me-render halaman sukses secara langsung, menekan tombol refresh akan memunculkan dialog “kirim ulang form?” yang menjengkelkan. Pola yang benar adalah POST-Redirect-GET: controller menerima POST, menyimpan data, lalu me-redirect ke halaman lain — dan pesan konfirmasinya “menumpang” session dalam bentuk flash data yang hidup tepat satu request. Pada chapter ini kita bangun pola tersebut dari nol, membuktikan umur satu request-nya lewat test, lalu membungkusnya menjadi komponen Blade yang rapi.
Menyimpan Flash dengan with() #
Cukup satu method di rantai redirect untuk menanam pesan:
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
Route::post('/post', function (Request $request) {
$request->validate([
'judul' => ['required', 'min:3'],
]);
// ... logika menyimpan post ...
return redirect()
->route('post.index')
->with('status', 'Post "Tulisan pertama" berhasil dibuat.');
})->name('post.store');
Route::get('/post', function () {
return view('post-index');
})->name('post.index');Method with('status', ...) memanggil session()->flash('status', ...) di balik layar: nilai masuk ke payload session dan namanya dicatat di array _flash.new. Halaman tujuan tinggal memeriksa kunci tersebut di Blade:
{{-- resources/views/post-index.blade.php --}}
@if (session('status'))
<div class="flash flash-success">
{{ session('status') }}
</div>
@endif
<h1>Daftar Post</h1>Umur Tepat Satu Request #
Klaim “hidup satu request” perlu dibuktikan. Feature test berikut memainkan tiga request: POST form, GET halaman hasil redirect, lalu GET sekali lagi:
public function test_flash_hanya_hidup_satu_request(): void
{
// Request 1: POST form sukses -> redirect + flash
$this->post('/post', ['judul' => 'Tulisan pertama'])
->assertRedirect(route('post.index'));
// Request kedua: ikut redirect, pesan tampil
$halaman = $this->get('/post')->assertOk();
preg_match('#<div class="flash[^>]*>.*?</div>#s', $halaman->getContent(), $m);
fwrite(STDERR, 'request_2_render: ' . trim(preg_replace('/\s+/', ' ', $m[0])) . "\n");
// Request ketiga: pesan hilang
$r3 = $this->get('/post');
fwrite(STDERR, 'request_3: pesan_tampil=' . var_export(str_contains($r3->getContent(), 'flash-success'), true) . "\n");
}Output yang Dihasilkan:
request_2_render: <div class="flash flash-success"> Post "Tulisan pertama" berhasil dibuat. </div>
request_3: pesan_tampil=falseBaris pertama adalah potongan HTML sungguhan yang dirender Blade pada request kedua — lengkap dengan " hasil escaping {{ }} terhadap tanda kutip. Baris kedua membuktikan pesan tidak pernah tampil lagi pada request ketiga.
Mesin penuangannya bekerja di ageFlashData() yang dipanggil setiap kali session disimpan di akhir request: nama kunci dipindahkan dari _flash.new ke _flash.old, lalu pada penyimpanan berikutnya semua isi _flash.old dihapus. Efek bersihnya: nilai bertahan melewati satu redirect, lalu lenyap — tanpa kita perlu memanggil forget() manual.
reflash() dan keep() #
Ada kalanya flash perlu umur lebih panjang — misalnya redirect berantai dua langkah. Method reflash() mengembalikan seluruh flash data yang sedang menua kembali ke daftar hidup, sementara keep(['kunci']) melakukannya hanya untuk kunci tertentu:
Route::get('/flash/read-reflash', function (Request $request) {
$request->session()->reflash();
return 'flash diperbarui umurnya';
});Feature test membandingkan siklus normal dengan siklus yang menyisipkan reflash():
public function test_reflash_memperpanjang_umur_flash(): void
{
$this->get('/flash/set')->assertRedirect('/flash/read');
$this->get('/flash/read'); // pesan tampil
$this->get('/flash/read'); // pesan hilang
fwrite(STDERR, "flash_normal: request_2=ada, request_3=hilang\n");
$this->get('/flash/set')->assertRedirect('/flash/read');
$this->get('/flash/read-reflash'); // pesan tampil + reflash()
$this->get('/flash/read'); // pesan masih ada
$this->get('/flash/read'); // baru hilang di sini
fwrite(STDERR, "flash_reflash: request_2=ada, request_3=ada, request_4=hilang\n");
}Output yang Dihasilkan:
flash_normal: request_2=ada, request_3=hilang
flash_reflash: request_2=ada, request_3=ada, request_4=hilangTanpa reflash(), pesan mati setelah request kedua. Dengan reflash() dipanggil di tengah request kedua, umur pesan bergeser satu request — baru mati setelah request ketiga. Method keep() berperan sama tetapi selektif: hanya kunci yang disebutkan yang diperpanjang, berguna ketika satu halaman memproses beberapa flash sekaligus dan ingin mempertahankan sebagian saja.
Komponen x-flash #
Saat jumlah halaman bertambah, blok @if (session('status')) yang di-copy-paste menjadi sampah kode. Anonymous component menyelesaikannya sekali untuk semua:
{{-- resources/views/components/flash.blade.php --}}
@props(['name' => 'status', 'type' => 'success'])
@if ($message = session($name))
<div class="flash flash-{{ $type }}" role="alert">
{{ $message }}
</div>
@endifKomponen tersebut menerima nama kunci session lewat prop name dan gaya visual lewat type, lalu dipakai satu baris di layout mana pun:
<x-flash name="status" type="success" />Feature test yang mengarahkan flash ke halaman berkomponen memperlihatkan hasil render sesungguhnya:
$this->get('/flash/set')->assertRedirect('/flash/read');
$response = $this->get('/flash/component');
fwrite(STDERR, 'x_flash_render: ' . trim(preg_replace('/\s+/', ' ', $response->getContent())) . "\n");Output yang Dihasilkan:
x_flash_render: <!DOCTYPE html> <html lang="id"> <head> <meta charset="utf-8"> <title>Halaman Contoh Flash</title> </head> <body> <div class="flash flash-success" role="alert"> Pesan sekali pakai </div> <h1>Daftar Post</h1> <p>Belum ada post.</p> </body> </html>Div pesan muncul otomatis di posisi <x-flash /> tanpa halaman tahu apa pun tentang mekanisme session. Atribut role="alert" ikut terpasang, membantu pembaca layar mengumumkan pesan tersebut.
Flash Tersembunyi: errors dan old Input #
Flash data juga bekerja diam-diam di balik kegagalan validasi. Saat rule min:3 menolak judul "ab", Laravel me-redirect balik dengan dua flash sekaligus: _old_input (isi form untuk old('judul')) dan errors (kumpulan pesan error untuk $errors di Blade). Feature test berikut membedahnya:
$this->post('/post', ['judul' => 'ab']); // gagal validasi min:3
$errors = session('errors'); // error bag tersimpan sebagai array di session
fwrite(STDERR, 'setelah_gagal: old_input_judul=' . var_export(session('_old_input.judul'), true)
. ' ada_errors=' . var_export(session()->has('errors'), true) . "\n");
fwrite(STDERR, 'errors_judul: ' . json_encode($errors['default']['messages']['judul'] ?? null) . "\n");
$this->get('/post'); // request berikutnya
fwrite(STDERR, 'request_berikutnya: ada_errors=' . var_export(session()->has('errors'), true)
. ' old_input_judul=' . var_export(session('_old_input.judul'), true) . "\n");Output yang Dihasilkan:
setelah_gagal: old_input_judul='ab' ada_errors=true
errors_judul: ["The judul field must be at least 3 characters."]
request_berikutnya: ada_errors=false old_input_judul=NULLTepat setelah POST gagal, session memuat input lama dan error bag; satu request kemudian keduanya sudah dibersihkan — pola umur satu request yang sama. Perbedaannya dengan status: kunci-kunci internal ini dikelola framework, dan $errors di Blade sudah membacanya untuk kita lewat directive @error.
Flash data aman dari segi kebingungan data, tetapi bukan dari segi serangan — justru karena session begitu sentral bagi login dan CSRF, session ID menjadi target. Chapter Session Security membahas cara Laravel memutarnya, dan daftar lengkap API-nya ada di dokumentasi Session .