aura/session
Aura\Session is a standalone PHP session library with a clean, object-oriented interface for managing session segments, flash values, and CSRF protection. It integrates easily with any framework, including Laravel.
Aura.Session is a framework-agnostic session library — not a Laravel package. It has no service provider, no facade, and no config file. You get four things: lazy session_start(), named "segments" (namespaced buckets inside $_SESSION), next-request-only flash values, and a CSRF token generator. Everything else (cookie params, save handlers, php.ini) is still native PHP.
composer require aura/session
Register it as a singleton so you get one Session instance per request:
// app/Providers/AuraSessionServiceProvider.php
use Aura\Session\SessionFactory;
public function register(): void
{
$this->app->singleton(SessionFactory::class, fn () => new SessionFactory());
$this->app->singleton('aura.session', function ($app) {
$cookies = $_COOKIE; // newInstance() takes it by reference
return $app->make(SessionFactory::class)->newInstance($cookies);
});
}
Where to look first: Aura\Session\Session (top-level API), Aura\Session\Segment (what you'll actually call 90% of the time), Aura\Session\CsrfToken. If you're wiring this into a package, code against SessionInterface / SegmentInterface — those are the two abstractions worth depending on.
First use case — a flash message across a redirect, i.e. the thing you'd otherwise reach for session()->flash() for:
$segment = app('aura.session')->getSegment('App\Http\Controllers\OrdersController');
$segment->setFlash('status', 'Order #1042 queued.');
return redirect()->route('orders.index');
Then on the next request:
if ($segment->getFlash('status')) { ... } // returns once, then gone
The getSegment() name is a literal key inside $_SESSION, so use a class name or a stable dotted prefix ('App.Auth') — not 'default', which is where collisions happen.
Segment-per-concern, not one global bag. Segments are the whole point: $session->getSegment('App\Auth') and $session->getSegment('App\Cart') never see each other's keys, and $segment->clear() wipes one without touching the other. Prefer reading the segment once in a constructor/middleware and injecting it, rather than calling getSegment() at every use site.
Lazy start is your configuration window. Nothing is written to $_SESSION until the first read or write, so this is legal and is the intended pattern:
$session = app('aura.session');
// all of this runs before session_start() happens implicitly
$session->setCookieParams([
'lifetime' => 0,
'path' => '/',
'domain' => null,
'secure' => true,
'httponly' => true,
'samesite' => 'Lax',
]);
$session->start(); // explicit is better than implicit in a web app
$session->regenerateId(); // after successful login
Auth identity in a segment. Store only the user id (or a small struct), not the whole User model — the session is a serialized blob and stale Eloquent models will bite you:
$auth = $session->getSegment('App\Auth');
$auth->set('user_id', $user->id);
$auth->set('logged_in_at', time());
// middleware
$userId = $auth->get('user_id');
abort_unless($userId, 401);
$request->attributes->set('auth_user_id', $userId);
CSRF for standalone forms. Use $segment->getCsrfToken() so the token is scoped per feature, or $session->getCsrfToken() for a global one:
$csrf = $session->getSegment('App\Checkout')->getCsrfToken();
// render:
// <input type="hidden" name="_token" value="<?= htmlspecialchars($csrf->getValue()) ?>">
// verify:
if (! $csrf->isValid($request->input('_token'))) {
abort(419);
}
Always call getValue() when rendering, before any isValid() call — see the gotcha below.
Flash as a redirect contract. Pair setFlash() with a with()-style redirect helper so controllers don't repeat key names:
function flash_redirect(string $route, string $key, string $msg) {
app('aura.session')->getSegment('App.Ui')->setFlash($key, $msg);
return redirect()->route($route);
}
Testing. Depend on SegmentInterface in your classes and hand in an in-memory fake in tests; that's cheaper and more deterministic than booting session_start(). For CSRF determinism, construct Aura\Session\Session directly with a stub RandvalInterface so getValue() returns a known string.
Custom storage. Aura still goes through PHP's session machinery, so Redis/Memcached is session_set_save_handler(new RedisHandler(...), true) called before start() — you're not getting Laravel's SessionHandler stack, and you're not getting config/session.php.
Laravel and Aura both want $_SESSION. Laravel's StartSession middleware calls session_start() and Laravel's Store writes to $_SESSION in the default file/cookie driver. Two session libraries on one superglobal is a recipe for lost writes and confusing flash behaviour. Pick one per route group: either ->withoutMiddleware(\Illuminate\Session\Middleware\StartSession::class) on routes that use Aura, or don't register the Aura provider globally at all. Whichever you choose, ensure Aura's start() runs before anything else touches $_SESSION, because the flash rotation (__flash → __flash_old, __flash_new → __flash) happens at start time and silently won't run twice.
CSRF isValid() on a fresh session always fails. isValid() resolves the expected token by calling getValue(), which generates and stores one if none exists. If the user's first-ever POST arrives with no server-side token, you've already lost — and worse, you've now written a token that will invalidate the next render. Call getValue() on every GET that renders a form. Also: Laravel's @csrf blade directive emits Laravel's token, not Aura's, and VerifyCsrfToken will reject the request before your controller runs. Either $except those URIs or swap the middleware.
Flash is genuinely one request. setFlash() twice for the same key in one request replaces the value. getFlash() consumes. If a redirect chain happens (302 → 302 → 200), the message lands on the intermediate hop and disappears. And don't rely on reading a flash in the same request that set it — treat it as next-request-only even if it happens to work.
regenerateId() does not carry the cookie. Aura deletes the old session file, but you still own the Set-Cookie header in edge cases; if sessions seem to "reset" after login, check whether the browser sent back the new id. Same for destroy() — session_destroy() doesn't unset the session cookie, so a subsequent request resurrects an empty session under the same id. Unset the cookie yourself on logout.
Headers-already-sent. Lazy start helps, but any echo/dd()/debug output before the first segment write will produce session_start(): Cannot start session when headers already sent. In Laravel this usually means a middleware ordering problem or a stray var_dump — not a bug in the package.
newInstance() takes $_COOKIE by reference. Passing a literal or a function return value is a fatal error in strict setups. Assign to a local first ($cookies = $_COOKIE;) inside the container binding, as above, and be aware that mutating $_COOKIE later won't retroactively help.
No expiry, no encryption, no serialization policy. Aura stores plain values under $_SESSION keys prefixed by segment name. It does not encrypt, does not rotate, does not validate. Don't put tokens, PII you wouldn't put in a cookie, or anything you'd want Laravel's EncryptCookies to protect. And don't store a segment value that isn't serializable — you'll get an opaque failure at shutdown, not at set() time.
Version pinning. The 2.x line is stable but low-activity; check composer show aura/session and pin a minor. Newer releases added PHP 8 random_bytes handling and samesite support — if you're on an older 2.x, setCookieParams(['samesite' => ...]) may be silently ignored.
Extension point worth knowing: Aura\Session\RandvalInterface is the only injectable seam in the whole package. If you need deterministic CSRF tokens in tests or a custom RNG for compliance reasons, that's where you hook in — everything else is either a constructor arg on Session or a native PHP call.
How can I help you explore Laravel packages today?