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:

php
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:

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:

php
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:

text
request_2_render: <div class="flash flash-success"> Post &quot;Tulisan pertama&quot; berhasil dibuat. </div>
request_3: pesan_tampil=false

Baris pertama adalah potongan HTML sungguhan yang dirender Blade pada request kedua — lengkap dengan &quot; 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:

php
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():

php
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:

text
flash_normal: request_2=ada, request_3=hilang
flash_reflash: request_2=ada, request_3=ada, request_4=hilang

Tanpa 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:

blade
{{-- resources/views/components/flash.blade.php --}}
@props(['name' => 'status', 'type' => 'success'])

@if ($message = session($name))
    <div class="flash flash-{{ $type }}" role="alert">
        {{ $message }}
    </div>
@endif

Komponen tersebut menerima nama kunci session lewat prop name dan gaya visual lewat type, lalu dipakai satu baris di layout mana pun:

blade
<x-flash name="status" type="success" />

Feature test yang mengarahkan flash ke halaman berkomponen memperlihatkan hasil render sesungguhnya:

php
$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:

text
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:

php
$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:

text
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=NULL

Tepat 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 .