Sessions
Reading and writing session data, flash messages, regeneration, and why PHP's own sessions are not used.
session_start() and $_SESSION are process-global. Under a worker that means one visitor's data is still in memory when the next request arrives — a session leak with no symptoms until it is a breach. phporbit implements its own, per-request and disposable. The portability test fails the build if the session extension is used anywhere.
Using it
Inject Session; SessionMiddleware publishes it:
<?php
use PhpOrbit\Session\Session;
final class PreferencesController implements Handler
{
public function __construct(private readonly Session $session)
{
}
public function handle(ServerRequest $request): Response
{
$this->session->set('theme', 'dark');
return Response::redirect('/');
}
}Reading and writing
<?php
$session->set('theme', 'dark'); // string|int|float|bool
$session->get('theme'); // ?string
$session->getInt('items'); // ?int
$session->getBool('onboarded'); // bool
$session->has('theme'); // bool
$session->remove('theme');
$session->all(); // array<string, scalar>Storing objects would mean serialising user-influenced data and unserialising it later, which is a well-known route to remote code execution. Store an id and reload the object.
Flash messages
For the redirect-after-write pattern — write, flash, redirect, show once:
<?php
$this->session->flash('notice', 'Article published.');
return Response::redirect('/articles');<?php
// On the next request — reading also removes it
$notice = $this->session->takeFlash('notice');@if($notice !== null)
<p class="notice">{{ $notice }}</p>
@endifRegeneration
Issue a new session id whenever the privilege level changes:
<?php
$session->regenerate(); // keeps the data, returns the old idWithout this, an attacker who fixes a victim's session id before login still holds a valid id afterwards. Authenticator::login() does it for you, so calling that instead of writing the user id yourself is the safer habit.
The old session file is deleted, so the previous id stops working immediately.
Destroying
<?php
$session->destroy(); // empties it and removes the stored fileThe response also carries an expired cookie, so the browser drops its copy.
Configuration
<?php
use PhpOrbit\Session\FileSessionStore;
use PhpOrbit\Session\SessionMiddleware;
$app->middleware(new SessionMiddleware(
new FileSessionStore($storage . '/sessions'),
cookieName: 'orbit_session',
lifetimeSeconds: 7200,
sameSite: SameSite::Lax,
));SESSION_LIFETIME=7200
SESSION_COOKIE=orbit_sessionSessionMiddleware must run before CsrfMiddleware, which reads the token it holds. Register it first.
What the framework does for you
No session file for anonymous visitors
A session is only written once something is actually stored. Someone who reads one page and leaves does not leave a file behind.
Unknown ids are never adopted
A cookie naming a session that does not exist gets a new session, not that id. Honouring it is precisely what makes session fixation possible.
Ids never reach the filesystem unvalidated
Ids are 256 bits of hex and are matched against a strict pattern before any file operation. A crafted cookie cannot steer a read or a write out of the session directory.
Writes are atomic
Data is written to a temporary file and renamed, which is atomic on POSIX filesystems. A reader sees either the old session or the new one, never a half-written file — which matters under the built-in server and FrankenPHP, where requests can touch one session in quick succession. Files are stored at mode 0600.
Corrupt files fail soft
An unreadable or malformed session file is treated as no session at all. The visitor gets a fresh one rather than an error page.
Expiry
<?php
$store = new FileSessionStore($storage . '/sessions');
$removed = $store->collectGarbage(); // deletes expired files, returns the countExpiry is stored in the file, so an expired session is refused on read even if the file is still there. Run collectGarbage() from cron for housekeeping.
Another store
<?php
use PhpOrbit\Session\SessionStore;
final class RedisSessionStore implements SessionStore
{
public function read(string $id): ?array { /* … */ }
public function write(string $id, array $data, int $lifetimeSeconds): void { /* … */ }
public function destroy(string $id): void { /* … */ }
public function collectGarbage(): int { return 0; } // Redis expires keys itself
}Worth doing once you run more than one application server, since file sessions are local to a machine.
Worker safety
<?php
public function test_sessions_do_not_leak_between_visitors(): void
{
$app = $this->bootApplication();
$first = $app->handle(Requests::post('/preferences', 'theme=dark'));
$cookie = $this->sessionCookieFrom($first);
// A different visitor, same process.
$second = $app->handle(Requests::get('/preferences'));
self::assertStringNotContainsString('dark', $second->body);
}Tests like this live in tests/Worker/ and boot once, handling several requests — the only way this class of bug is visible. More →