DALT.PHP
Framework Deep Dive4. Session & State

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 gone

Think 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 limits

The Solution: Flash Data

Session::flash('errors', $errors);
redirect('/register');

// Request 1: Errors stored in flash
// Request 2: Errors available, then deleted
// Request 3: Errors gone

Perfect 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 here

The 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 gone

Key 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 exists

Request 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 gone

Request 3: GET /dashboard

// 1. Request starts
// 2. $_SESSION['_flash'] doesn't exist

// 3. Session::get('errors') returns null
// 4. No errors shown

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

  1. Calls Session::get('old')
  2. Gets the old array from flash
  3. Returns $old['email'] if it exists
  4. 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 gone

Key: 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] ?? $default

This 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');  // null

Flash 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

  1. Flash exists for one request - Perfect for form errors
  2. Stored under _flash key - Separate from regular data
  3. unflash() deletes it - Called at end of every request
  4. get() checks flash first - Flash has priority
  5. 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.

On this page