Lab 4: Session Middleware and Flash Messages
Additional Resources
Section titled “Additional Resources”- Middleware in Slim 4
- Session Middleware
- Flash Messages in Slim 4
- Working with the BaseController class
- PHP Session Documentation ↗
- OWASP Session Management Cheat Sheet ↗
- Post-Redirect-Get Pattern ↗
- Bootstrap Alerts Documentation ↗
Overview
Section titled “Overview”In this lab, you will add session support to your e-commerce application using a Slim 4 middleware, then use the session to build a flash messaging system. A flash message is one-time feedback shown to the user after an action, such as “Product saved successfully” after a form submission. By the end of this lab, every page in the admin panel you built in Lab 3 will be able to display flash messages as Bootstrap alerts.
Learning Objectives
Section titled “Learning Objectives”By completing this lab, you will:
- Understand how application-level middleware runs before every route in Slim 4.
- Create a
SessionManagerhelper class that starts sessions with secure settings. - Create and register a
SessionMiddlewareso sessions are available in every controller. - Implement a
FlashMessagehelper class that stores messages in the session and clears them once displayed. - Combine flash messages with the BaseController’s
redirect()method to apply the Post-Redirect-Get (PRG) pattern. - Display flash messages as Bootstrap alerts in the admin layout.
Prerequisites
Section titled “Prerequisites”Before starting this lab, ensure you have:
- Completed Lab 3: Building an Admin Panel. Part III of this lab uses the admin layout (
adminHeader.phpandadminFooter.php), theViewHelper::loadAdminHeader()method, and the/adminroute group from Lab 3. - The BaseController class set up, including its
render()andredirect()methods. - Read Middleware in Slim 4 to understand how a middleware wraps a request.
Part I: Session Middleware
Section titled “Part I: Session Middleware”PHP only gives your code access to $_SESSION after session_start() has been called during the current request. In this part, you will create a middleware that starts the session once, before any route runs, so your controllers never need to call session_start() themselves.
Step 1: Create the SessionManager Helper Class
Section titled “Step 1: Create the SessionManager Helper Class”Objective: Create a helper class that will manage all session operations with built-in security features.
Instructions:
- Navigate to your
app/Helpers/directory - Create a new file named
SessionManager.php - Copy and paste the following code:
<?php
namespace App\Helpers;
class SessionManager{ /** * Start the session with secure configuration. */ public static function start(): void { if (session_status() === PHP_SESSION_NONE) {
// Configure session to last 1 day (good for local development) $sessionLifetime = 24 * 60 * 60; // (hours * minutes * seconds) -> 1 day in seconds -> 86400 seconds
// Set session cookie parameters. session_set_cookie_params([ 'lifetime' => $sessionLifetime, 'path' => '/', 'domain' => '', 'secure' => isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on', 'httponly' => true, 'samesite' => 'Strict' ]);
// Configure garbage collection (automatic cleanup) ini_set('session.gc_maxlifetime', $sessionLifetime); ini_set('session.gc_probability', 1); ini_set('session.gc_divisor', 100);
// Security settings. ini_set('session.use_strict_mode', 1); ini_set('session.cookie_httponly', 1);
session_start();
// Regenerate session ID every 15 minutes for security. if (!isset($_SESSION['last_regeneration'])) { $_SESSION['last_regeneration'] = time(); session_regenerate_id(true); } elseif (time() - $_SESSION['last_regeneration'] > 900) { $_SESSION['last_regeneration'] = time(); session_regenerate_id(true); }
// Browser fingerprinting for security (detect session hijacking). $fingerprint = self::generateFingerprint(); if (!isset($_SESSION['fingerprint'])) { $_SESSION['fingerprint'] = $fingerprint; } elseif ($_SESSION['fingerprint'] !== $fingerprint) { // Different browser detected - destroy and restart session. session_destroy(); session_start(); $_SESSION['fingerprint'] = $fingerprint; $_SESSION['last_regeneration'] = time(); } } }
/** * Generate a unique browser fingerprint for security. */ private static function generateFingerprint(): string { $factors = [ $_SERVER['HTTP_USER_AGENT'] ?? '', $_SERVER['HTTP_ACCEPT_LANGUAGE'] ?? '', $_SERVER['HTTP_ACCEPT_ENCODING'] ?? '' ];
return hash('sha256', implode('|', $factors)); }
/** * Store a value in the session. */ public static function set(string $key, $value): void { $_SESSION[$key] = $value; }
/** * Retrieve a value from the session. */ public static function get(string $key, $default = null) { return $_SESSION[$key] ?? $default; }
/** * Check if a key exists in the session. */ public static function has(string $key): bool { return isset($_SESSION[$key]); }
/** * Remove a key from the session. */ public static function remove(string $key): void { unset($_SESSION[$key]); }
/** * Clear all session data. */ public static function clear(): void { session_unset(); }
/** * Destroy the session completely. */ public static function destroy(): void { session_destroy(); }}- Save the file
What you just created:
- A SessionManager class with secure session configuration
- Helper methods for common session operations (
set,get,has,remove,clear,destroy) - Built-in security features (session ID regeneration, browser fingerprinting)
- Automatic cleanup of old session files
Step 2: Create the SessionMiddleware Class
Section titled “Step 2: Create the SessionMiddleware Class”Objective: Create a middleware that starts the session for every request.
Instructions:
- Navigate to your
app/Middleware/directory - Create a new file named
SessionMiddleware.php - Copy and paste the following code:
<?php
namespace App\Middleware;
use App\Helpers\SessionManager;use Psr\Http\Server\MiddlewareInterface;use Psr\Http\Message\ResponseInterface as Response;use Psr\Http\Message\ServerRequestInterface as Request;use Psr\Http\Server\RequestHandlerInterface as Handler;
class SessionMiddleware implements MiddlewareInterface{ public function process( Request $request, Handler $handler ): Response { // Start session using SessionManager SessionManager::start();
// Continue to next middleware/route return $handler->handle($request); }}- Save the file
What you just created:
- A middleware that implements the PSR-15
MiddlewareInterface - A
process()method that starts the session, then passes the request on to the next middleware or route by calling$handler->handle($request)
Step 3: Register the Middleware
Section titled “Step 3: Register the Middleware”Objective: Add the SessionMiddleware to your application so it runs on every request.
Instructions:
- Open your
config/middleware.phpfile - Add the following import at the top of the file (if not already present):
use App\Middleware\SessionMiddleware;- Add the middleware to your application by adding this line:
// Add the session middleware to the application (applies to ALL routes)$app->add(SessionMiddleware::class);- Save the file
Step 4: Test the Session
Section titled “Step 4: Test the Session”Objective: Verify that the session keeps its data between requests.
Instructions:
- Open your
app/Routes/web-routes.phpfile - Add the following import at the top:
use App\Helpers\SessionManager;- Add this test route inside the
return static function (Slim\App $app): void { ... }closure:
$app->get('/test-session', function ($request, $response) { // Get current counter, increment it $counter = SessionManager::get('counter', 0) + 1; SessionManager::set('counter', $counter);
$response->getBody()->write("Counter: " . $counter);
return $response;});- Save the file
- Visit
http://localhost/your-app-name/test-sessionin your browser (replaceyour-app-namewith your project’s folder name inhtdocs/)
Expected Results:
- First visit: “Counter: 1”
- Refresh page: “Counter: 2”
- Refresh again: “Counter: 3”
If the counter always shows “1”:
- Check that SessionMiddleware is registered in
config/middleware.php - Verify that your browser accepts cookies
- Clear your browser cookies for
localhostand test again
Part II: Flash Messages
Section titled “Part II: Flash Messages”A flash message is stored in the session during one request and displayed, then deleted, during the next request. Flash messages are used with the Post-Redirect-Get (PRG) pattern:
- The browser sends a POST request (for example, a form submission).
- The controller processes the data, adds a flash message to the session, and redirects to another page.
- The browser follows the redirect with a GET request. The page displays the flash message and removes it from the session.
Because the last request is a GET, refreshing the page does not resubmit the form, and the flash message does not appear a second time.
Step 5: Create the FlashMessage Helper Class
Section titled “Step 5: Create the FlashMessage Helper Class”Objective: Create a helper class that manages flash messages stored in the session.
Instructions:
- Navigate to your
app/Helpers/directory - Create a new file named
FlashMessage.php - Copy and paste the following code:
<?php
namespace App\Helpers;
class FlashMessage{ // All flash messages are stored in $_SESSION under this key. private const FLASH_KEY = 'flash_messages';
/** * Add a success message. * * This method is provided as an example for error(), info(), and warning(). */ public static function success(string $message): void { self::add('success', $message); }
/** * Add an error message. */ public static function error(string $message): void { // TODO }
/** * Add an info message. */ public static function info(string $message): void { // TODO }
/** * Add a warning message. */ public static function warning(string $message): void { // TODO }
/** * Add a flash message of the given type to the session. * * Messages are queued: adding a message must keep any messages * that are already stored in the session. */ public static function add(string $type, string $message): void { // TODO }
/** * Return all pending flash messages and remove them from the session, * so that each message is displayed only once. * * Returns an empty array when there are no messages. */ public static function get(): array { // TODO }
/** * Return true if at least one flash message is waiting to be displayed. * * Unlike get(), this method must not remove any messages. */ public static function has(): bool { // TODO }
/** * Remove all pending flash messages without returning them. */ public static function clear(): void { // TODO }
/** * Render all flash messages as Bootstrap alerts. * * This method is provided for you as it involves complex HTML generation. */ public static function render(bool $dismissible = true): string { $messages = self::get(); if (empty($messages)) { return ''; }
$bootstrapTypes = [ 'success' => 'success', 'error' => 'danger', 'info' => 'info', 'warning' => 'warning' ];
$html = ''; foreach ($messages as $flash) { $type = $bootstrapTypes[$flash['type']] ?? 'info'; $message = htmlspecialchars($flash['message']);
if ($dismissible) { $html .= <<<HTML <div class="alert alert-{$type} alert-dismissible fade show" role="alert"> {$message} <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button> </div> HTML; } else { $html .= <<<HTML <div class="alert alert-{$type}" role="alert"> {$message} </div> HTML; } }
return $html; }}- Implement every method marked
// TODO, following the behavior described in its comment - Save the file
Session data format: render() reads the messages in the following format, so add() must store them the same way. $_SESSION['flash_messages'] holds a list of messages, and each message is an associative array with a type key and a message key:
$_SESSION['flash_messages'] = [ ['type' => 'success', 'message' => 'Product saved.'], ['type' => 'warning', 'message' => 'Stock is running low.'],];Use the self::FLASH_KEY constant instead of typing 'flash_messages' in each method.
What you need to implement:
error(),info(),warning(): follow thesuccess()exampleadd(): appends one message to the list in the sessionget(): returns the list and removes it from the sessionhas(): checks for pending messages without removing themclear(): removes the list without returning it
Already provided:
- The
success()method, as an example of a message-type method - The
render()method that generates Bootstrap alert HTML with XSS protection
You will test has() in Step 6 and clear() in Step 10.
Part III: Display Flash Messages in the Admin Panel
Section titled “Part III: Display Flash Messages in the Admin Panel”In this part, you will render flash messages in the admin layout from Lab 3, then build a small demo page in the admin panel to test each message type.
Step 6: Render Flash Messages in the Admin Layout
Section titled “Step 6: Render Flash Messages in the Admin Layout”Objective: Display pending flash messages at the top of every admin page.
Instructions:
- Open
app/Views/admin/adminHeader.php - Find the placeholder comment you added in Lab 3:
<?php // Flash messages will be rendered here in a later lab. ?>- Replace it with the following code:
<!-- Flash Messages --><?php if (App\Helpers\FlashMessage::has()): ?> <div class="mb-4"> <?= App\Helpers\FlashMessage::render() ?> </div><?php endif; ?>- Save the file
The has() check adds the spacing container only when messages are waiting, so pages without messages have no empty gap above their content. has() must not remove the messages; otherwise render() would find nothing to display.
Step 7: Create the FlashDemoController
Section titled “Step 7: Create the FlashDemoController”Objective: Create a controller that adds flash messages and redirects, following the PRG pattern.
Instructions:
- Navigate to your
app/Controllers/directory - Create a new file named
FlashDemoController.php - Start from the following class structure:
<?php
namespace App\Controllers;
use App\Helpers\FlashMessage;use DI\Container;use Psr\Http\Message\ResponseInterface as Response;use Psr\Http\Message\ServerRequestInterface as Request;
class FlashDemoController extends BaseController{ public function __construct(Container $container) { parent::__construct($container); }
// TODO: Implement the methods below}- Implement the following methods, using the standard Slim callback signature
(Request $request, Response $response, array $args): Response:
| Method | What It Should Do |
|---|---|
index() | Render the admin/flashDemoView.php view. Pass a $data array with the keys title ('Flash Messages Demo') and username ($_SESSION['username'] ?? 'Admin'). |
success() | Add a success flash message, then redirect to the admin.flash route |
error() | Add an error flash message, then redirect to the admin.flash route |
info() | Add an info flash message, then redirect to the admin.flash route |
warning() | Add a warning flash message, then redirect to the admin.flash route |
multiple() | Add 2 or 3 flash messages of different types, then redirect to the admin.flash route |
clearMessages() | Add 2 flash messages of any type, call FlashMessage::clear(), then add one info message with the text “Queued messages were cleared.” and redirect to the admin.flash route |
- Save the file
Hints:
- Use
FlashMessage::success(),FlashMessage::error(),FlashMessage::info(), orFlashMessage::warning() - In
clearMessages(), the order of the calls matters: only the info message added afterclear()should reach the next page - Return
$this->redirect($request, $response, 'admin.flash')after setting the flash messages - The message text can be anything descriptive (for example, “This is a success message!”)
- The
titleandusernamekeys are the same ones you used for the dashboard in Lab 3, because the view passes them toViewHelper::loadAdminHeader()
Step 8: Register the Routes
Section titled “Step 8: Register the Routes”Objective: Add the demo routes to the admin route group.
Instructions:
- Open your
app/Routes/web-routes.phpfile - Add the following import at the top:
use App\Controllers\FlashDemoController;- Add the following routes inside the
$app->group('/admin', ...)callback you created in Lab 3, after the dashboard route:
// Flash message demo routes$group->get('/flash', [FlashDemoController::class, 'index']) ->setName('admin.flash');$group->post('/flash/success', [FlashDemoController::class, 'success']) ->setName('admin.flash.success');$group->post('/flash/error', [FlashDemoController::class, 'error']) ->setName('admin.flash.error');$group->post('/flash/info', [FlashDemoController::class, 'info']) ->setName('admin.flash.info');$group->post('/flash/warning', [FlashDemoController::class, 'warning']) ->setName('admin.flash.warning');$group->post('/flash/multiple', [FlashDemoController::class, 'multiple']) ->setName('admin.flash.multiple');$group->post('/flash/clear', [FlashDemoController::class, 'clearMessages']) ->setName('admin.flash.clear');- Save the file
Routes Created:
| Method | URI | Controller Method | Route Name |
|---|---|---|---|
| GET | /admin/flash | FlashDemoController::index() | admin.flash |
| POST | /admin/flash/success | FlashDemoController::success() | admin.flash.success |
| POST | /admin/flash/error | FlashDemoController::error() | admin.flash.error |
| POST | /admin/flash/info | FlashDemoController::info() | admin.flash.info |
| POST | /admin/flash/warning | FlashDemoController::warning() | admin.flash.warning |
| POST | /admin/flash/multiple | FlashDemoController::multiple() | admin.flash.multiple |
| POST | /admin/flash/clear | FlashDemoController::clearMessages() | admin.flash.clear |
Step 9: Create the Demo View
Section titled “Step 9: Create the Demo View”Objective: Create an admin view with buttons that test each flash message type.
Instructions:
- Inside
app/Views/admin/, create a file namedflashDemoView.php - Copy and paste the following code:
<?php
use App\Helpers\ViewHelper;
ViewHelper::loadAdminHeader($title, $username);?>
<div class="card"> <div class="card-header"> <h5 class="mb-0">Test Flash Messages</h5> </div> <div class="card-body"> <p class="card-text">Click the buttons below to test different types of flash messages:</p>
<div class="d-grid gap-2"> <form method="POST" action="<?= APP_BASE_URL ?>/admin/flash/success"> <button type="submit" class="btn btn-success w-100">Show Success Message</button> </form>
<form method="POST" action="<?= APP_BASE_URL ?>/admin/flash/error"> <button type="submit" class="btn btn-danger w-100">Show Error Message</button> </form>
<form method="POST" action="<?= APP_BASE_URL ?>/admin/flash/info"> <button type="submit" class="btn btn-info w-100">Show Info Message</button> </form>
<form method="POST" action="<?= APP_BASE_URL ?>/admin/flash/warning"> <button type="submit" class="btn btn-warning w-100">Show Warning Message</button> </form>
<form method="POST" action="<?= APP_BASE_URL ?>/admin/flash/multiple"> <button type="submit" class="btn btn-primary w-100">Show Multiple Messages</button> </form>
<form method="POST" action="<?= APP_BASE_URL ?>/admin/flash/clear"> <button type="submit" class="btn btn-secondary w-100">Clear Queued Messages</button> </form> </div> </div></div>
<?php ViewHelper::loadAdminFooter(); ?>- Save the file
What you just created:
- A view that uses the admin layout, so flash messages appear in the area you added to
adminHeader.phpin Step 6 - One POST form per demo action, each submitting to one of the routes from Step 8
Step 10: Test Your Flash Messages
Section titled “Step 10: Test Your Flash Messages”- Visit
http://localhost/your-app-name/admin/flashin your browser - Click each button and check the alert that appears at the top of the page:
- “Show Success Message”: green alert
- “Show Error Message”: red alert
- “Show Info Message”: blue alert
- “Show Warning Message”: yellow alert
- “Show Multiple Messages”: several alerts at once
- “Clear Queued Messages”: only the blue “Queued messages were cleared.” alert. If the two earlier messages also appear,
clear()is not removing them.
- Click the close button (×) on an alert. The alert should disappear.
- Show any message, then refresh the page. The message should not appear again, and the browser should not ask you to confirm a form resubmission.
If no message appears, check the following in order: the SessionMiddleware is registered (Step 4 works), has() returns true when messages are pending, add() and get() use the session data format from Step 5, the controller redirects to admin.flash, and adminHeader.php calls FlashMessage::render().
Understanding What You Built
Section titled “Understanding What You Built”Request Flow
Section titled “Request Flow”The following diagram shows the two requests involved when you click “Show Success Message”:
Request 1: POST /admin/flash/success ↓SessionMiddleware (starts the session) ↓FlashDemoController::success() ↓ FlashMessage::success() stores the message in $_SESSION302 redirect to /admin/flash ↓Request 2: GET /admin/flash ↓SessionMiddleware (resumes the same session) ↓FlashDemoController::index() renders flashDemoView.php ↓ adminHeader.php calls FlashMessage::render(), which reads and removes the messagesResponse sent to browserVerification Checklist
Section titled “Verification Checklist”Before submitting your lab, verify the following:
-
SessionManager.phpandFlashMessage.phpexist inapp/Helpers/ -
SessionMiddleware.phpexists inapp/Middleware/ - SessionMiddleware is registered in
config/middleware.php - The
/test-sessioncounter increases with each refresh -
adminHeader.phprenders flash messages - Each button on
/admin/flashshows an alert with the correct color - “Clear Queued Messages” shows only the info alert
- Refreshing the page after a message removes the message