Skip to content

Implementing a Session Middleware in Slim 4 Framework

  • Session middleware automatically starts PHP sessions for your entire application.
  • Instead of calling session_start() in every route, the middleware does it for you once: before any route executes.

Why use it?

  • ✅ Write session startup code once.
  • ✅ No risk of forgetting session_start().
  • ✅ No errors from starting sessions multiple times.
  • ✅ Sessions available everywhere automatically.

To properly implement session middleware, we’ll need to create and use two components:

  1. SessionManager: Handles session configuration and security.
  2. SessionMiddleware: Starts or resumes sessions for every request.

The SessionManager class will handle the session configuration and security.

  • It must be created in a file named SessionManager.php in the app/Helpers/ directory.
SessionManager.php
<?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'] ?? '',
$_SERVER['REMOTE_ADDR'] ?? ''
];
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();
}
}

Key functionalities:

  • 1-day session lifetime: Sessions last 24 hours (convenient for development).
  • Secure cookie settings: Protects against XSS and ensures HTTPS when available.
  • Garbage collection: Automatically cleans up old session files (1% chance per request).
  • Session ID regeneration: Changes session ID every 15 minutes to prevent hijacking.
  • Browser fingerprinting: Detects if session cookie is stolen and used on different browser.
  • Helper methods: Convenient methods to work with session data:
MethodDescription
set($key, $value)Store data in the session
get($key, $default)Retrieve data from the session (returns default if key doesn’t exist)
has($key)Check if a key exists in the session
remove($key)Remove a specific key from the session
clear()Clear all session data (keeps session alive)
destroy()Destroy the session completely

The SessionMiddleware class will start or resume sessions for every request.

  • It must be created in a file named SessionMiddleware.php in the app/Middleware/ directory.
SessionMiddleware.php
<?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);
}
}

How it works?

  • Middleware calls SessionManager::start() for every request
  • SessionManager handles all configuration and security automatically
  • Session is ready to use in all routes

To register the session middleware, we need to create an instance of the SessionMiddleware class and add it to the application.

This should be done in the (config/middleware.php) file:

// Add the session middleware to the application (applies to ALL routes).
$app->add(SessionMiddleware::class);

That’s it! Sessions now work in all your routes.


Add a new route to test the session middleware. This route will increment a counter and store it in the session.

The following code should be added to the web-routes.php file:

use App\Helpers\SessionManager;
$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;
});

Or using traditional $_SESSION:

$app->get('/test-session', function ($request, $response) {
$_SESSION['counter'] = ($_SESSION['counter'] ?? 0) + 1;
$response->getBody()->write(
"Counter: " . $_SESSION['counter']
);
return $response;
});

Expected results:

  • Visit /test-session → Counter: 1
  • Refresh page → Counter: 2
  • Refresh again → Counter: 3

If it always shows “Counter: 1”, sessions aren’t persisting (see troubleshooting below).


Once registered, you can use session data in two ways:

Section titled “Option 1: Using SessionManager (Recommended)”

Cleaner and more object-oriented approach using controller methods:

use App\Helpers\SessionManager;
use App\Controllers\BaseController;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
class AuthController extends BaseController
{
// Store data
public function login(Request $request, Response $response, array $args): Response
{
// Process login logic here
SessionManager::set('user_id', 123);
SessionManager::set('username', 'john');
// Redirect to dashboard after login
return $this->redirect($request, $response, 'dashboard');
}
// Read data
public function profile(Request $request, Response $response, array $args): Response
{
$userId = SessionManager::get('user_id');
if (!$userId) {
// Not logged in - redirect to login page
return $this->redirect($request, $response, 'login');
}
// User is logged in - render profile view
$username = SessionManager::get('username');
return $this->render($response, 'auth/profile.php', [
'user_id' => $userId,
'username' => $username
]);
}
// Check if logged in
public function dashboard(Request $request, Response $response, array $args): Response
{
if (!SessionManager::has('user_id')) {
// Not logged in - redirect to login page
return $this->redirect($request, $response, 'login');
}
// User is logged in - render dashboard
return $this->render($response, 'auth/dashboard.php', [
'username' => SessionManager::get('username')
]);
}
// Logout (destroy session)
public function logout(Request $request, Response $response, array $args): Response
{
SessionManager::destroy();
// Redirect to home page after logout
return $this->redirect($request, $response, 'home.index');
}
}
class CartController extends BaseController
{
// Remove specific key
public function clearCart(Request $request, Response $response, array $args): Response
{
SessionManager::remove('cart_items');
// Redirect back to cart page
return $this->redirect($request, $response, 'cart.index');
}
}

Traditional PHP approach (still works):

// Store data
$app->post('/login', function ($request, $response) {
$_SESSION['user_id'] = 123;
$_SESSION['username'] = 'john';
return $response;
});
// Read data
$app->get('/profile', function ($request, $response) {
$userId = $_SESSION['user_id'] ?? null;
if (!$userId) {
return $response->withStatus(401);
}
return $response;
});
// Logout
$app->post('/logout', function ($request, $response) {
session_destroy();
return $response;
});

Recommendation: Use SessionManager methods for cleaner, more maintainable code.


Section titled “1. Use SessionManager Methods (Recommended)”
use App\Helpers\SessionManager;
// ✅ Best - clean and safe
$userId = SessionManager::get('user_id');
SessionManager::set('cart_total', 99.99);
// ✅ Good - with default value
$userId = SessionManager::get('user_id', null);
// ✅ Check if exists
if (SessionManager::has('user_id')) {
// User is logged in
}

Or using $_SESSION directly:

// ✅ Good - prevents errors
$userId = $_SESSION['user_id'] ?? null;
// ❌ Bad - throws error if not set
$userId = $_SESSION['user_id'];
// ✅ Good
SessionManager::set('user_id', 123);
SessionManager::set('cart_items', []);
// ❌ Bad
SessionManager::set('u', 123);
SessionManager::set('data', []);
use App\Helpers\SessionManager;
// ✅ Recommended - clear all data and destroy
SessionManager::destroy();
// ✅ Alternative - just clear data
SessionManager::clear();
// Traditional way (still works)
session_unset();
session_destroy();

All available methods for working with sessions:

use App\Helpers\SessionManager;
// Start session (called automatically by middleware)
SessionManager::start();
// Store data
SessionManager::set('key', 'value');
SessionManager::set('user_id', 123);
SessionManager::set('cart', ['item1', 'item2']);
// Retrieve data
$value = SessionManager::get('key'); // Returns value or null
$userId = SessionManager::get('user_id', 0); // Returns value or default (0)
$cart = SessionManager::get('cart', []); // Returns array or empty array
// Check if key exists
if (SessionManager::has('user_id')) {
// Key exists in session
}
// Remove specific key
SessionManager::remove('cart');
SessionManager::remove('user_id');
// Clear all session data (but keep session active)
SessionManager::clear();
// Destroy session completely (logout)
SessionManager::destroy();

Common patterns:

// Login
SessionManager::set('user_id', $user->id);
SessionManager::set('username', $user->username);
// Check if logged in
if (!SessionManager::has('user_id')) {
// Redirect to login
}
// Get user data
$userId = SessionManager::get('user_id');
$username = SessionManager::get('username', 'Guest');
// Logout
SessionManager::destroy();

What you created:

  1. SessionManager (app/Helpers/SessionManager.php)

    • Handles all session configuration and security settings.
    • Includes automatic session ID regeneration.
    • Detects and prevents session hijacking.
    • Provides convenient helper methods: set(), get(), has(), remove(), clear(), destroy().
  2. SessionMiddleware (app/Middleware/SessionMiddleware.php)

    • Triggers SessionManager for every request.
    • Makes sessions available in all routes.
  3. Registered middleware in config/middleware.php


Symptoms: Counter always shows 1, session data disappears

Solutions:

  • ✅ Verify middleware is registered in config/middleware.php
  • ✅ Check browser accepts cookies (check browser settings)
  • ✅ Ensure session save path is writable: ls -la /tmp (Linux/Mac) or check C:\Windows\Temp (Windows)

Issue 2: Fingerprint Mismatch (Session Resets)

Section titled “Issue 2: Fingerprint Mismatch (Session Resets)”

Symptoms: Session resets unexpectedly, user logged out randomly

Causes:

  • Browser extensions changing headers
  • VPN switching IP addresses
  • Mobile switching between WiFi/cellular

Solutions:

  • For development, comment out the fingerprinting code in SessionManager::start() (lines 85-95)
  • For production, keep it enabled for security

Symptoms: Session expires before 24 hours

Solution:

// Change session lifetime in SessionManager.php
$sessionLifetime = 7 * 24 * 60 * 60; // 7 days instead of 1 day