Flash Data
How temporary session data works for form errors and messages
Temporary Memory
Flash data is session data that exists for exactly one request:
Request 1: Store flash data
Request 2: Read flash data (available)
Request 3: Flash data is goneThink of it like a sticky note:
- Write a message
- Stick it on the fridge
- Someone reads it
- They throw it away
Why Flash Data Exists
The Problem
User submits a form with errors:
// POST /register
$errors = ['email' => 'Email is required'];
// How do we show errors on the form page?
// We need to redirect back to GET /register
// But how do we pass the errors?Can't use regular session:
$_SESSION['errors'] = $errors;
redirect('/register');
// Problem: Errors stay forever
// User fixes form, submits successfully
// Visits /register again later
// Still sees old errors!Can't use URL parameters:
redirect('/register?errors=' . json_encode($errors));
// Problems:
// - URL gets very long
// - Errors visible in URL (ugly, insecure)
// - URL length limitsThe Solution: Flash Data
Session::flash('errors', $errors);
redirect('/register');
// Request 1: Errors stored in flash
// Request 2: Errors available, then deleted
// Request 3: Errors gonePerfect for one-time messages!
The flash() Method
public static function flash($key, $value)
{
$_SESSION['_flash'][$key] = $value;
}Usage:
Session::flash('errors', ['email' => 'Invalid email']);
Session::flash('message', 'Post created successfully!');
Session::flash('old', ['email' => 'user@example.com']);What it does:
- Stores data under
$_SESSION['_flash'] - Separate from regular session data
- Marked for deletion after next request
The structure:
$_SESSION = [
'user' => ['email' => 'user@example.com'], // Regular
'_flash' => [ // Flash
'errors' => ['email' => 'Invalid'],
'old' => ['email' => 'test@example.com']
]
];The unflash() Method
public static function unflash()
{
unset($_SESSION['_flash']);
}When it's called:
Remember from Part 1, at the end of public/index.php:
try {
$router->route($uri, $method, $request);
} catch (ValidationException $exception) {
Session::flash('errors', $exception->errors);
Session::flash('old', $exception->old);
redirect($router->previousUrl());
}
Session::unflash(); // ← Called hereThe flow:
Request 1: Form submission
↓
Validation fails
↓
Session::flash('errors', ...)
↓
redirect('/register')
↓
Response sent
↓
Session::unflash() NOT called (redirect exits early)
Request 2: Show form
↓
Session::get('errors') returns flash data
↓
View displays errors
↓
Response sent
↓
Session::unflash() called ← Flash data deleted here
Request 3: Any page
↓
Session::get('errors') returns null
↓
Flash data is goneKey insight: unflash() runs at the end of every request, but flash data survives one request because it's set AFTER the current request's unflash().
Flash Data Lifecycle
Detailed Timeline
Request 1: POST /register (validation fails)
// 1. Request starts
// 2. Validation fails
ValidationException::throw($errors, $old);
// 3. Caught in index.php
catch (ValidationException $e) {
Session::flash('errors', $e->errors); // Set flash
Session::flash('old', $e->old);
redirect('/register'); // Exit here
}
// 4. Session::unflash() never runs (redirect exited)
// 5. $_SESSION['_flash'] still existsRequest 2: GET /register (show form)
// 1. Request starts
// 2. $_SESSION['_flash'] exists from previous request
// 3. View reads flash data
$errors = Session::get('errors'); // Reads from _flash
$old = old('email'); // Reads from _flash
// 4. Response sent
// 5. Session::unflash() runs
unset($_SESSION['_flash']); // Flash deleted
// 6. $_SESSION['_flash'] is goneRequest 3: GET /dashboard
// 1. Request starts
// 2. $_SESSION['_flash'] doesn't exist
// 3. Session::get('errors') returns null
// 4. No errors shownReading Flash Data
Via Session::get()
$errors = Session::get('errors');Remember, get() checks flash first:
return $_SESSION['_flash'][$key] ?? $_SESSION[$key] ?? $default;So flash data is automatically available through get().
Via old() Helper
For form inputs, there's a helper:
function old($key, $default = '')
{
return Core\Session::get('old')[$key] ?? $default;
}Usage in views:
<input
type="email"
name="email"
value="<?= htmlspecialchars(old('email')) ?>"
>What this does:
- Calls
Session::get('old') - Gets the
oldarray from flash - Returns
$old['email']if it exists - Returns
''if not
Why this is useful:
Without old():
<input
type="email"
name="email"
value="<?= htmlspecialchars(Session::get('old')['email'] ?? '') ?>"
>With old():
<input
type="email"
name="email"
value="<?= htmlspecialchars(old('email')) ?>"
>Much cleaner!
Flash Data in Practice
Form Validation Flow
1. User submits form
<form method="POST" action="/register">
<input name="email" value="">
<input name="password" value="">
<button>Register</button>
</form>2. Controller validates
// app/Http/controllers/registration/store.php
$email = $_POST['email'];
$password = $_POST['password'];
$errors = [];
if (!Validator::email($email)) {
$errors['email'] = 'Invalid email format';
}
if (!Validator::string($password, 7, 255)) {
$errors['password'] = 'Password must be at least 7 characters';
}
if (!empty($errors)) {
ValidationException::throw($errors, $_POST);
}
// If we get here, validation passed
// Create user...3. ValidationException is thrown
class ValidationException extends \Exception
{
public $errors;
public $old;
public static function throw($errors, $old)
{
$instance = new static;
$instance->errors = $errors;
$instance->old = $old;
throw $instance;
}
}4. Caught in index.php
catch (ValidationException $exception) {
Session::flash('errors', $exception->errors);
Session::flash('old', $exception->old);
redirect($router->previousUrl());
}5. User sees form again with errors
// resources/views/registration/create.view.php
$errors = Session::get('errors', []);
<form method="POST" action="/register">
<input
name="email"
value="<?= htmlspecialchars(old('email')) ?>"
>
<?php if (isset($errors['email'])): ?>
<p class="error"><?= $errors['email'] ?></p>
<?php endif; ?>
<input
name="password"
value="<?= htmlspecialchars(old('password')) ?>"
>
<?php if (isset($errors['password'])): ?>
<p class="error"><?= $errors['password'] ?></p>
<?php endif; ?>
<button>Register</button>
</form>Result:
- Email field is pre-filled with what user typed
- Error messages show next to fields
- User can fix and resubmit
Flash Messages (Success/Info)
Flash isn't just for errors:
// After creating a post
Session::flash('message', 'Post created successfully!');
redirect('/posts');
// In the view
<?php if ($message = Session::get('message')): ?>
<div class="success">
<?= htmlspecialchars($message) ?>
</div>
<?php endif; ?>Common patterns:
// Success message
Session::flash('success', 'Account created!');
// Info message
Session::flash('info', 'Please verify your email');
// Warning message
Session::flash('warning', 'Your trial expires soon');
// Error message
Session::flash('error', 'Something went wrong');Why Flash Works
The Timing
Flash data survives exactly one redirect:
POST /register
↓
Set flash
↓
Redirect (exit before unflash)
↓
GET /register
↓
Read flash
↓
unflash() at end
↓
Flash goneKey: The redirect exits before unflash() runs, so flash survives to the next request.
The Priority
Session::get() checks flash first:
$_SESSION['_flash'][$key] ?? $_SESSION[$key] ?? $defaultThis means flash always overrides regular session data for one request.
Edge Cases
Multiple Redirects
// Request 1
Session::flash('message', 'Hello');
redirect('/page1');
// Request 2: /page1
redirect('/page2'); // Flash still exists
// Request 3: /page2
Session::get('message'); // Returns 'Hello'
// unflash() runs
// Flash deleted
// Request 4
Session::get('message'); // nullFlash survives multiple redirects until a request completes normally.
Flash Overwrite
Session::flash('message', 'First');
Session::flash('message', 'Second'); // Overwrites
// Next request
Session::get('message'); // Returns 'Second'Last flash wins.
Flash + Regular Session
$_SESSION['message'] = 'Regular';
Session::flash('message', 'Flash');
// Next request
Session::get('message'); // Returns 'Flash' (flash has priority)
// Request after that
Session::get('message'); // Returns 'Regular' (flash is gone)Key Takeaways
- Flash exists for one request - Perfect for form errors
- Stored under _flash key - Separate from regular data
- unflash() deletes it - Called at end of every request
- get() checks flash first - Flash has priority
- Survives redirects - Because redirect exits early
What's Good Here
✅ Simple implementation (just a special key)
✅ Automatic cleanup (unflash at request end)
✅ Perfect for form validation UX
✅ Works with redirects
Design Note
DALT intentionally keeps flash “one request only” because it’s the easiest mental model for learning. If you ever need “keep/reflash” behavior later, you can add it, but it’s not required to understand how sessions work.