Sessions
A session is a per-user, server-side bag of data that persists across requests. Lift's session system is:
- Driver-based — file, database, Redis, Memcached, or in-memory drivers.
- Cookie-driven — only an opaque session ID lives in the cookie; the data stays server-side.
- PSR-15-friendly — the session is exposed as a request attribute, so your handlers stay testable.
Mental model:
SessionMiddlewarereads the session ID from a cookie, loads the data through a backing store, attaches theSessionobject to the request, then writes any changes back on the way out.
The simplest setup
For prototypes — file-backed sessions, no database required:
use Lift\App;
use Lift\Http\Session\FileSessionStore;
use Lift\Http\Session\Session;
use Lift\Http\Session\SessionMiddleware;
$app = new App();
$store = new FileSessionStore(__DIR__ . '/../storage/sessions');
$session = new Session($store, lifetime: 7200, cookieName: 'my_session');
$app->use(new SessionMiddleware($session));
// In any handler:
$app->get('/me', function (Request $req) {
$session = $req->getAttribute('session'); // Lift\Http\Session\Session
return ['user_id' => $session->get('user_id')];
});
Make sure storage/sessions/ exists and is writable by the web-server user.
What the middleware does, per request
- Reads the session ID from the
my_sessioncookie (or generates a new one if missing). - Hydrates the session by calling
Store::read($id). - Attaches the
Sessionobject to the request as'session'attribute. - Calls your handler.
- In a
finallyblock: ages flash data, then writes viaStore::write($id, …, ttl). - Appends a
Set-Cookieheader so the browser keeps the same ID next time.
Even when your handler throws, the session is still persisted (step 5).
Reading and writing
Once attached, the Session API is small and obvious:
$session = $req->getAttribute('session');
$session->get('key', $default = null);
$session->set('key', $value);
$session->has('key'); // bool
$session->pull('key'); // get + delete in one call
$session->forget('key', 'another'); // delete one or more keys
$session->all(); // entire data array
Chainable:
$session
->set('user_id', 42)
->set('last_seen', time());
Flash messages
A flash message is a value that lives exactly one extra request — perfect for "action succeeded" notifications shown after a redirect.
// In the handler that processed a form POST:
$session->flash('notice', 'User created.');
return Response::redirect('/users');
// On the redirected /users page:
$notice = $session->pull('notice'); // 'User created.' on the first read, gone afterwards
How it works: flash() writes the value normally and marks the key in _flash_new. After the handler runs, ageFlashData() moves _flash_new → _flash_old so the value survives one more request. On the next ageFlashData() call, anything in _flash_old is deleted.
Session ID regeneration
Always regenerate the session ID right after a privilege change (login, role escalation) to prevent session-fixation attacks:
$app->post('/login', function (Request $req) {
$session = $req->getAttribute('session');
// …authenticate the user…
$session->regenerate(); // ID rotated, old session deleted from store
$session->set('user_id', $user->id);
return Response::redirect('/dashboard');
});
Pass $deleteOldSession: false if you want to keep the old data accessible elsewhere — almost never the right choice.
Since 1.2.1: as a defence-in-depth measure, when the session ID comes from a client cookie and the store holds no session under it,
start()mints a fresh ID instead of adopting the client-supplied value. This does not replace callingregenerate()on login — an attacker can still fixate a valid session before authentication — but it stops a fixed unknown ID from being adopted outright.
Destroying a session (logout)
$app->post('/logout', function (Request $req) {
$req->getAttribute('session')->destroy();
return Response::redirect('/');
});
destroy() clears the data and deletes the entry from the store.
Available drivers
All drivers implement SessionStoreInterface. Pick one based on where you want the data.
FileSessionStore
new FileSessionStore(__DIR__ . '/../storage/sessions');
Stores one file per session ID. Good for single-server, low-traffic apps. Run a periodic GC task (store->gc(7200)) so expired files are reaped — or run it inline at the start of each request if you don't care about a few ms of latency.
DatabaseSessionStore
use Lift\Database\Connection;
use Lift\Http\Session\DatabaseSessionStore;
$db = Connection::fromConfig([...]);
// Create the `sessions` table once (or run `lift migrate` if you generated a migration):
(new \Lift\Database\Migrator($db, '...'))->createSessionsTable();
new DatabaseSessionStore($db, table: 'sessions');
Survives across servers. Slowest of the four (every read/write is a SQL round-trip).
RedisSessionStore
use Lift\Http\Session\RedisSessionStore;
use Lift\Redis\RedisClient;
$redis = new RedisClient(host: 'redis', port: 6379);
new RedisSessionStore($redis, prefix: 'sess:');
Native TTL, sub-millisecond access. The default for any horizontally-scaled deployment.
MemcachedSessionStore
new MemcachedSessionStore($memcached); // ext-memcached instance
Like Redis but uses Memcached. Has no persistence — fine for sessions, not for queues.
ArraySessionStore
new ArraySessionStore();
In-memory only, lost when the process dies. Perfect for tests.
Custom stores
Implement Lift\Http\Session\SessionStoreInterface:
interface SessionStoreInterface
{
public function read(string $id): ?string;
public function write(string $id, string $payload, int $ttl): void;
public function destroy(string $id): void;
public function gc(int $maxLifetime): void;
}
$payload is an opaque PHP-serialised string — your store treats it as a blob.
Cookie attributes
When the middleware writes the cookie, it uses these defaults:
| Attribute | Default | Override |
|---|---|---|
Path |
/ |
hard-coded |
HttpOnly |
always | hard-coded |
SameSite |
Lax |
hard-coded |
Max-Age |
$lifetime (default 7200 s) |
new Session($store, lifetime: …) |
Secure |
only on HTTPS | auto-detected from $req->getUri()->getScheme() |
If you need different cookie attributes (e.g. SameSite=Strict, a parent domain, etc.), build a custom middleware or subclass SessionMiddleware.
Security checklist
- ✅ Always use HTTPS in production. The session cookie is the most security-critical part of your stack.
- ✅ Call
$session->regenerate()on login / privilege change. - ✅ Call
$session->destroy()on logout. - ✅ For sensitive data, don't put it in the session — only an opaque user ID. Look the rest up server-side on each request.
- ✅ Set a reasonable
lifetime. 2 hours is the default; 30 minutes is safer for admin areas.
Since 1.3.0: object deserialisation is disabled by default (
allowedClasses: false). This keeps tampered or stale session payloads from instantiating application classes.
- ❌ Don't serialise objects with secrets into the session — pass the allowed-classes whitelist or store IDs only:
new Session($store); // no objects by default new Session($store, allowedClasses: [Money::class]); // explicit allowlist
Common pitfalls
| Symptom | Cause | Fix |
|---|---|---|
| Session is empty on every request | Middleware not registered, or wrong cookie name | $app->use(new SessionMiddleware($session)); and check cookieName. |
| Login works locally but not in prod | Cookie's Secure flag is set, but you're on HTTP |
Use HTTPS, or set up a TLS-terminating reverse proxy. |
| Data lost across two servers | File store + multiple app servers | Switch to Redis/DB. |
unserialize security warnings |
You stored an object whose class is no longer loadable | Use allowedClasses: false and store scalars only. |
| Flash message doesn't appear | You called flash() then read it on the same request |
Flash is for next request — redirect first, read after. |
| Session "logged out" on POST | CSRF middleware regenerated the ID; or you re-used the old $session reference after regenerate() |
Re-fetch with $req->getAttribute('session') after sensitive changes. |
Cheat sheet
// Boot
$store = new FileSessionStore($path); // or Redis/DB/Memcached
$session = new Session($store, lifetime: 7200);
$app->use(new SessionMiddleware($session));
// Use
$session = $req->getAttribute('session');
$session->set('user_id', 42);
$session->get('user_id');
$session->pull('flash');
$session->flash('notice', 'OK');
$session->regenerate(); // after login
$session->destroy(); // on logout