SessionManager provides a clean, full-featured interface over PHP native sessions. It covers everything from basic key/value storage to CSRF protection and flash messages — all in one class.
- Class:
JiFramework\Core\Session\SessionManager - Access:
$app->sessionManager
Features at a glance:
- Full session CRUD —
set(),get(),has(),delete(),all(),clear(),destroy() - CSRF protection — token generation, verification, and a one-line middleware
- Flash messages — one-time notifications that survive exactly one redirect
- Session ID regeneration to prevent session fixation attacks
The session is started automatically during new App() via Config::initSession(). You do not need to call start() manually in normal usage.
$app = new App();
$sm = $app->sessionManager;
// Store and retrieve
$sm->set('user_id', 42);
$id = $sm->get('user_id'); // 42
// Flash and redirect
$sm->flashSuccess('Profile updated.');
$app->redirect('/profile');
Configuration keys (set in config/jiconfig.php):
session_name— cookie name (default:PHPSESSID)session_lifetime— cookie lifetime in seconds (default:0= until browser closes)csrf_token_key— session key for CSRF token store (default:_ji_csrf_token)csrf_token_expiry— token lifetime in seconds (default:3600)csrf_token_limit— max simultaneous tokens per session (default:10)flash_message_key— session key for flash message store (default:_ji_flash)
start(): void
Starts the PHP session if it is not already active. Safe to call multiple times — it checks the session status first and does nothing if a session is already running.
In normal usage the session is started automatically during new App(), so you will rarely need to call this directly. It is useful if you have disabled the auto-start or are writing a script that may or may not have an active session.
$sm = $app->sessionManager;
// Start only if not already active
$sm->start();
// Safe to call again - no double-start
$sm->start();
$sm->start(); // still fine
// Typical scenario: ensure session before a conditional early-exit script
if (php_sapi_name() === 'cli') {
// ... CLI task
exit;
}
$sm->start();
$sm->set('cli_ran_at', date('Y-m-d H:i:s'));
isStarted(): bool
Returns true if a session is currently active. Wraps session_status() === PHP_SESSION_ACTIVE.
if ($sm->isStarted()) {
echo 'Session is active. ID: ' . $sm->id();
} else {
echo 'No active session.';
}
// Guard before performing session operations
if (!$sm->isStarted()) {
$sm->start();
}
$sm->set('key', 'value');
// After destroy() the session is no longer active
$sm->destroy();
var_dump($sm->isStarted()); // bool(false)
id(): string
Returns the current session ID as a string. Returns an empty string if no session is active. Wraps session_id().
$sessionId = $sm->id();
echo $sessionId; // e.g. "abc123def456..."
// Log the session ID for debugging
$app->logger->debug('Request session', ['sid' => $sm->id()]);
// Check ID changed after regeneration (in a script that runs before output)
$before = $sm->id();
$sm->regenerateSession();
$after = $sm->id();
// $before !== $after (when called before headers are sent)
set(string $key, mixed $value): void
Store a value in the session under the given key. Any serializable PHP value is accepted: strings, integers, floats, booleans, arrays, and null. If the key already exists its value is overwritten.
$key— (string) The session key$value— (mixed) Any serializable PHP value
// Scalar values
$sm->set('user_id', 42);
$sm->set('username', 'alice');
$sm->set('is_admin', true);
$sm->set('last_score', 98.5);
// Array (e.g. user profile cached in session)
$sm->set('user', [
'id' => 42,
'name' => 'Alice',
'email' => '[email protected]',
'role' => 'editor',
]);
// Null (key still exists - has() returns true)
$sm->set('pending_action', null);
// Overwrite an existing key
$sm->set('cart_count', 1);
$sm->set('cart_count', 2); // now 2
// Nest structured data
$sm->set('filters', [
'status' => 'active',
'category' => 5,
'sort' => 'name_asc',
]);
get(string $key, mixed $default = null): mixed
Retrieve a value from the session by key. If the key does not exist, returns $default (which itself defaults to null). Note: if a key was explicitly set to null, get() returns null but the key does still exist — use has() to distinguish between "key is missing" and "key is null".
$key— (string) The session key$default— (mixed, optional) Returned when the key does not exist. Default:null
// Basic retrieval
$userId = $sm->get('user_id');
// With a fallback default
$theme = $sm->get('theme', 'light');
$language = $sm->get('language', 'en');
$page = $sm->get('current_page', 1);
// Array stored in session
$user = $sm->get('user');
if ($user) {
echo 'Hello, ' . $user['name'];
}
// Auth guard
$userId = $sm->get('user_id');
if (!$userId) {
$app->redirect('/login');
}
// Null vs missing key
$sm->set('flag', null);
$sm->get('flag'); // null (key exists, value is null)
$sm->get('missing'); // null (key does not exist)
$sm->get('missing', 'x'); // "x" (default used because key is absent)
has(string $key): bool
Returns true if the session key exists, even if its value is null. Uses array_key_exists() internally — not isset() — so a key explicitly set to null is correctly reported as present.
$key— (string) The session key to check
$sm->set('user_id', 42);
$sm->set('flag', null);
$sm->has('user_id'); // true - key exists, value 42
$sm->has('flag'); // true - key exists, value null
$sm->has('nonexistent'); // false - key does not exist
// Practical: conditional logic that must handle null values
if ($sm->has('pending_upload')) {
// process even if pending_upload === null
processPending($sm->get('pending_upload'));
}
// Gate behind has() before get() to avoid ambiguity
if ($sm->has('filters')) {
$filters = $sm->get('filters');
applyFilters($filters);
}
delete(string $key): void
Remove a single key from the session. Safe to call even if the key does not exist — no error is thrown.
$key— (string) The session key to remove
// Remove a single key
$sm->delete('temp_token');
// Safe even if key never existed
$sm->delete('nonexistent_key'); // no error
// Remove on logout (partial cleanup)
$sm->delete('user_id');
$sm->delete('user');
$sm->delete('cart');
// Pattern: consume a one-time value
if ($sm->has('redirect_after_login')) {
$url = $sm->get('redirect_after_login');
$sm->delete('redirect_after_login');
$app->redirect($url);
}
all(): array
Return the entire $_SESSION superglobal as an associative array. Returns an empty array if no session is active. Useful for debugging, serializing session state, or iterating all keys.
$data = $sm->all();
// Count how many keys are stored
echo count($data) . ' session keys';
// Dump for debugging
var_dump($sm->all());
// Check whether a particular internal key exists in the raw session
$all = $sm->all();
if (isset($all['_ji_csrf_token'])) {
echo count($all['_ji_csrf_token']) . ' CSRF tokens stored';
}
// Log full session state
$app->logger->debug('Session state', $sm->all());
clear(): void
Wipe all data from the current session by setting $_SESSION = []. The session remains active — the same session ID is kept, the cookie is unchanged, and isStarted() still returns true. Use destroy() instead if you want to fully terminate the session.
// Remove all session data but keep session alive
$sm->clear();
var_dump($sm->all()); // []
var_dump($sm->isStarted()); // bool(true) - still active
// Common use: reset state without destroying the session (e.g. after a failed wizard step)
$sm->clear();
$sm->set('wizard_step', 1);
// Clear then repopulate
$sm->clear();
$sm->set('user_id', $newUserId);
$sm->set('role', 'viewer');
destroy(): void
Fully terminate the session. This performs three steps in order:
- Sets
$_SESSION = []to clear all data - Expires the session cookie on the client (only if headers have not yet been sent)
- Calls
session_destroy()to delete the server-side session file
After destroy(), isStarted() returns false. Call this on logout.
// Logout handler (call before any output)
$sm->destroy();
var_dump($sm->isStarted()); // bool(false)
var_dump($_SESSION); // []
$app->redirect('/login');
// Full logout flow
function logout($app) {
$sm = $app->sessionManager;
// 1. Verify CSRF to prevent logout CSRF attacks
$sm->csrfMiddleware('/');
// 2. Destroy session
$sm->destroy();
// 3. Redirect
$app->redirect('/login?logged_out=1');
}
Note: The cookie expiry call is guarded with headers_sent(). If output has already started (e.g. in a debug context), the server-side session is still destroyed — only the client cookie will not be explicitly expired.
regenerateSession(bool $deleteOldSession = true): bool
Generate a new session ID while preserving all existing session data. Call this immediately after a privilege change (login, role elevation) to prevent session fixation attacks.
$deleteOldSession— (bool, optional) Whentrue(default), the old session file is deleted from the server. Whenfalse, the old file is kept.
Returns true on success, false if headers are already sent (triggers E_USER_WARNING in that case).
Important: Must be called before any output because it needs to set a new session cookie header.
// After successful login - call before any output
function handleLogin(array $user, $app) {
$sm = $app->sessionManager;
// 1. Regenerate ID to prevent session fixation
$sm->regenerateSession(); // deletes old session file by default
// 2. Store authenticated user
$sm->set('user_id', $user['id']);
$sm->set('user_role', $user['role']);
$sm->flashSuccess('Welcome back, ' . $user['name'] . '!');
// 3. Redirect
$app->redirect('/dashboard');
}
// Keep old session data (e.g. cart items set before login)
$sm->regenerateSession(false);
// Check return value
if (!$sm->regenerateSession()) {
// Headers already sent - log a warning and continue
$app->logger->warning('Could not regenerate session ID: headers already sent');
}
setSessionCookieParams(array $params = []): void
Configure the session cookie before the session starts. Must be called before new App() — once the session is active it has no effect and triggers an E_USER_WARNING.
Default values applied when a key is omitted:
lifetime—0(until browser closes)path—/domain—""(current domain)secure—trueif$_SERVER['HTTPS']is set, otherwisefalsehttponly—truesamesite—"Lax"
<?php
require __DIR__ . '/vendor/autoload.php';
use JiFramework\Core\App\App;
use JiFramework\Core\Session\SessionManager;
// Configure BEFORE new App()
$sm = new SessionManager();
$sm->setSessionCookieParams([
'lifetime' => 3600, // 1 hour
'secure' => true, // HTTPS only
'samesite' => 'Strict', // no cross-site sending
]);
$app = new App(); // session starts here with the params above
// Calling after App() triggers a warning and does nothing
$sm->setSessionCookieParams(['lifetime' => 7200]);
// PHP Warning: [JiFramework] setSessionCookieParams() has no effect after session_start()...
generateCsrfToken(): string
Generate a cryptographically random CSRF token, store it in the session with a timestamp, and return it. Multiple tokens can coexist simultaneously — this supports tabbed browsing where each tab may load a different form.
Returns a 64-character hexadecimal string (32 random bytes via random_bytes()).
The session stores each token alongside its creation timestamp for expiry checking. When the token limit is reached (default: 10), the oldest token is removed to make room for the new one.
// Generate a token for a form
$token = $sm->generateCsrfToken();
Embed it in your HTML form:
<form method="POST" action="/submit">
<input type="hidden" name="_csrf_token" value="<?= htmlspecialchars($token) ?>">
<input type="text" name="title">
<button type="submit">Save</button>
</form>
// Tabs example - each form gets its own token
$tokenA = $sm->generateCsrfToken(); // form on page A
$tokenB = $sm->generateCsrfToken(); // form on page B
// Both are stored in session and both verify correctly
$sm->verifyCsrfToken($tokenA); // true
$sm->verifyCsrfToken($tokenB); // true
// AJAX - inject into page and send as header
$token = $sm->generateCsrfToken();
<meta name="csrf-token" content="<?= htmlspecialchars($token) ?>">
<script>
// Attach to every AJAX request automatically
const csrfToken = document.querySelector('meta[name="csrf-token"]').content;
fetch('/api/action', {
method: 'POST',
headers: { 'X-CSRF-TOKEN': csrfToken }
});
</script>
verifyCsrfToken(string $token = ''): bool
Verify a CSRF token against the session store. Returns true if the token exists and has not expired, false otherwise.
$token— (string, optional) Token to verify. If empty, falls back to theX-CSRF-TOKENrequest header automatically — this enables AJAX requests to send the token as a header instead of a POST field.
Tokens are not single-use — a valid token remains in the session and can be verified multiple times until it expires (default: 1 hour) or the session is cleared. Expired tokens are cleaned up on each call.
$token = $_POST['_csrf_token'] ?? '';
if ($sm->verifyCsrfToken($token)) {
// Token is valid - process the form
} else {
// Token missing, expired, or tampered
$app->redirect('/form?error=csrf');
}
// Empty token falls back to X-CSRF-TOKEN header (AJAX support)
// If the fetch sends: headers: { 'X-CSRF-TOKEN': token }
$valid = $sm->verifyCsrfToken(); // reads header automatically
// Verify a specific token multiple times (not consumed)
$t = $sm->generateCsrfToken();
$sm->verifyCsrfToken($t); // true
$sm->verifyCsrfToken($t); // still true
// Invalid or tampered token
$sm->verifyCsrfToken('not-a-real-token'); // false
$sm->verifyCsrfToken(''); // false (no header present either)
csrfMiddleware(string $redirectOnFail = '/'): void
One-line CSRF enforcement for any POST handler. Call it at the top of a page or route and it handles everything: it reads the token, verifies it, and responds appropriately if invalid. Non-POST requests pass through immediately.
$redirectOnFail— (string, optional) URL to redirect to on failure for regular HTML form requests. Default:/
On CSRF failure:
- AJAX / JSON requests — returns HTTP 403 with
{"error": "CSRF token invalid or expired."}then exits - Regular form requests — stores a flash error, redirects to
$redirectOnFail, then exits
AJAX detection checks for: X-Requested-With: XMLHttpRequest, Accept: application/json, or Content-Type: application/json.
The middleware reads the token from $_POST['_csrf_token'] and falls back to the X-CSRF-TOKEN header for AJAX.
// Form handler - redirect to /contact on failure
$sm->csrfMiddleware('/contact');
// form processing continues here if CSRF passed
// API endpoint - AJAX clients get JSON 403 on failure
$sm->csrfMiddleware();
// Full example: POST form handler
$sm->csrfMiddleware('/profile/edit');
$rules = ['name' => 'required|min:2', 'email' => 'required|email'];
if (!$app->validator->validate($_POST, $rules)) {
$sm->flashError('Please fix the errors below.');
$app->redirect('/profile/edit');
}
$app->db->table('users')
->where('id', $sm->get('user_id'))
->update(['name' => $_POST['name'], 'email' => $_POST['email']]);
$sm->flashSuccess('Profile updated.');
$app->redirect('/profile');
setFlashMessage(string $type, string $message, array $data = []): void
Store a one-time notification in the session. Flash messages survive exactly one page load — they are retrieved once via getFlashMessages() and then automatically cleared from the session.
$type— (string) One of:'success','error','info','warning'. Any invalid type falls back to'info'.$message— (string) The notification text$data— (array, optional) Extra context data attached to the message
// Basic flash messages
$sm->setFlashMessage('success', 'Your profile has been updated.');
$sm->setFlashMessage('error', 'The file could not be uploaded.');
$sm->setFlashMessage('info', 'Your session will expire in 5 minutes.');
$sm->setFlashMessage('warning', 'You have unsaved changes.');
// With extra data - useful for linking to a created resource
$sm->setFlashMessage('success', 'Post published.', ['post_id' => $newId]);
// Invalid type falls back to info
$sm->setFlashMessage('notice', 'Something happened.');
// stored as type = 'info'
// Multiple messages of different types can coexist
$sm->setFlashMessage('success', 'Changes saved.');
$sm->setFlashMessage('warning', 'Email verification is pending.');
$app->redirect('/dashboard');
getFlashMessages(): array
Retrieve all pending flash messages and clear them from the session in a single call. Returns an array of message entries, each with type, message, and data keys. Returns an empty array if no messages are pending.
$messages = $sm->getFlashMessages();
// Structure of each entry:
// [
// 'type' => 'success', // 'success', 'error', 'info', or 'warning'
// 'message' => 'Profile saved.', // the notification text
// 'data' => [], // optional extra data
// ]
// After retrieval, messages are gone from the session
$again = $sm->getFlashMessages();
// $again === []
Render all pending messages in your layout or header template:
$messages = $app->sessionManager->getFlashMessages();
foreach ($messages as $msg) {
$type = htmlspecialchars($msg['type']);
$text = htmlspecialchars($msg['message']);
echo "<div class="alert alert-{$type}">{$text}</div>";
}
// Access extra data when present
foreach ($messages as $msg) {
if ($msg['type'] === 'success' && isset($msg['data']['post_id'])) {
$id = (int)$msg['data']['post_id'];
echo "Post created: <a href="/posts/{$id}">View it</a>";
}
}
Four convenience wrappers over setFlashMessage() for the most common notification types. Each maps directly to its type string and supports an optional $data array.
flashError(string $message, array $data = []): void
flashSuccess(string $message, array $data = []): void
flashInfo(string $message, array $data = []): void
flashWarning(string $message, array $data = []): void
// Error
$sm->flashError('Invalid credentials. Please try again.');
// Success with extra data
$sm->flashSuccess('Invoice #' . $invoiceId . ' created.', ['id' => $invoiceId]);
// Info
$sm->flashInfo('Your export is being processed. Check back in a minute.');
// Warning
$sm->flashWarning('Your subscription expires in 3 days.');
// Typical patterns
// --- Login failure
if (!$passwordOk) {
$sm->flashError('Incorrect email or password.');
$app->redirect('/login');
}
// --- Successful form save
$app->db->table('settings')->where('user_id', $uid)->update($data);
$sm->flashSuccess('Settings saved.');
$app->redirect('/settings');
// --- Delete confirmation
$app->db->table('posts')->where('id', $postId)->delete();
$sm->flashSuccess('Post deleted.');
$app->redirect('/posts');
// --- Missing permission
if (!$isAdmin) {
$sm->flashError('You do not have permission to do that.');
$app->redirect('/dashboard');
}