bryanjhv/slim-session
A session middleware for Slim 4 built on PHP's native session handling, with flash messages, session segments, ID rotation, and configurable cookie/storage options. PSR-7/PSR-15 compatible and easy to integrate into Slim applications.
What it is: a ~200-line Slim 4 middleware plus a static helper that wires PHP's native session machinery into Slim's PSR-7 request/response cycle. It is not a Laravel package — as a Laravel developer you'll meet it when you inherit a Slim 4 microservice, a Slim-based admin/tool app, or a small service sitting inside an otherwise-Laravel estate. The mental model maps almost 1:1 onto Laravel's session: Session::get/set/delete ≈ session(), regenerateId() ≈ $request->session()->regenerate().
Install:
composer require bryanjhv/slim-session
Register once, in the app bootstrap, before any middleware that needs the session:
use Slim\Factory\AppFactory;
use Slim\Middleware\Session;
$app = AppFactory::create();
$app->add(new Session([
'name' => 'app_session', // cookie name
'autorefresh' => true, // slide expiry on every request
'lifetime' => '2 hours', // accepts strtotime-style strings
'secure' => true, // HTTPS only — see Gotchas
'httponly' => true,
'samesite' => 'Lax',
]));
Ordering matters: Slim 4's $app->add() stack is LIFO, so the last middleware added executes first. Add the session middleware so it wraps the handlers that depend on it (auth, CSRF, flash), and keep it outside addRoutingMiddleware() if you want it available to other middleware too.
First use case — a login flag and a guarded route:
use Slim\Session;
$app->post('/login', function ($request, $response) {
if (check_credentials($request->getParsedBody())) {
Session::regenerateId(); // kill fixation before trusting the session
Session::set('user_id', 42);
return $response->withHeader('Location', '/dashboard')->withStatus(302);
}
Session::set('error', 'Bad credentials');
return $response->withHeader('Location', '/login')->withStatus(302);
});
$app->get('/dashboard', function ($request, $response) {
if (!Session::exists('user_id')) {
return $response->withHeader('Location', '/login')->withStatus(302);
}
return $response;
});
Where to look first: the GitHub README (options table), then src/Middleware/Session.php for the options array and defaults, then src/Session.php for the helper surface (get, set, delete, exists, destroy, regenerateId). The whole thing is small enough to read end-to-end before you depend on it — do that, it takes ten minutes and answers every "does it support X?" question.
Auth guard as middleware. Extract the Session::exists('user_id') check into a dedicated middleware and attach it per route group rather than repeating it:
$auth = function ($request, $handler) {
if (!\Slim\Session::exists('user_id')) {
return (new \Slim\Psr7\Response())
->withHeader('Location', '/login')->withStatus(302);
}
return $handler->handle($request);
};
$app->group('/admin', function ($group) { /* routes */ })->add($auth);
Flash messages. There is no Laravel-style flash() here. The standard idiom is store-then-consume:
Session::set('flash', ['error' => 'Saved failed']);
// next request, in a view middleware:
$flash = Session::get('flash', []);
Session::delete('flash');
Wrap that pair in a tiny helper class so every controller doesn't reimplement it.
CSRF. Generate a token on GET into Session::set('csrf', bin2hex(random_bytes(32))), compare on POST with hash_equals, and regenerate on login. Pair it with your routing/error middleware rather than sprinkling checks in handlers.
Custom storage for anything multi-node. The default file handler serializes per-session on disk and will quietly break behind multiple app servers or containers. The middleware accepts a handler option taking any SessionHandlerInterface — pass a Redis or database-backed handler and you get shared sessions plus proper locking behaviour for free:
$app->add(new Session([
'handler' => new RedisSessionHandler($redis),
'lifetime' => '8 hours',
]));
Wrap the static helper. Because Slim\Session is a static facade over global state, it's awkward to inject and awkward to test. Define your own SessionService that delegates to it, register it in the container, and type-hint that in handlers. You get constructor injection, easy fakes in tests, and a single place to swap implementations later.
Don't session-ify stateless APIs. Apply the middleware only to browser-facing route groups. Slapping it on a JSON API means Set-Cookie on every response, which ruins CDN caching and confuses clients.
View integration. For Twig/Blade-style templates, add a small middleware that computes user, flash, and csrf_token from the session once and stores them as request attributes or view globals — otherwise templates reach into the static helper, which is untestable.
Testing. Build a ServerRequestFactory request, run the app, and assert on the Set-Cookie header. In setUp() call Session::destroy() and reset any static instance state, or session data leaks between test cases.
secure => true on http://localhost is the #1 "sessions never persist" bug. The browser silently drops the cookie. Toggle it via environment ('secure' => $config->isHttps()) rather than hardcoding.session_start() and output. If anything echoes before Slim writes headers, PHP emits "headers already sent" and the session cookie never lands. Enable output buffering in dev to surface this early.samesite => 'Lax' breaks cross-site POSTs — payment provider callbacks, OAuth redirects, embedded iframes. Use None + Secure for those flows, and be aware older browsers ignore None without Secure.session_write_close()) once you've finished reading.lifetime is not gc_maxlifetime. The package controls the cookie's lifetime; PHP's garbage collector uses session.gc_maxlifetime from php.ini. On shared hosting they diverge and users get logged out early. Align them, and note that autorefresh => true re-sends Set-Cookie on every response — fine for apps, hostile to caching layers.Slim\ (Slim\Middleware\Session, Slim\Session), the same root as Slim core. IDE auto-import will grab the wrong Session class regularly, and any of your own Slim\* classes risk colliding. Import explicitly and review imports in PRs.app.example.com and api.example.com requires 'domain' => '.example.com'. Changing name invalidates every existing cookie — everyone gets logged out; treat it as a breaking config change.Set-Cookie in the response, then Session::getId(), then session_save_path() and directory permissions (/var/lib/php/sessions is a classic permission trap in Docker images).composer.json, read the source before upgrading, and keep the wrapper service described above so replacing it with odan/session or ~100 lines of your own middleware is a contained change rather than a rewrite.session_start() while this middleware is active; you'll get two competing session lifecycles and very confusing data loss.How can I help you explore Laravel packages today?