Sessions & Cookies
Two components cover per-visitor state. Pop\Session\Session wraps $_SESSION and adds namespaces plus
values that clean themselves up; Pop\Cookie\Cookie wraps setcookie() and $_COOKIE. Both are singletons
you fetch rather than construct, and both read and write through property and array access, so the code you
write looks the same in a controller, a model or a middleware handler.
Reading and Writing Session Data#
Session::getInstance() starts the PHP session if one is not running and hands back the object. Call it
wherever you need it — you get the same instance every time within a request.
$session = Pop\Session\Session::getInstance();
$session->user_id = 1001;
$session['role'] = 'editor';
Reading back is the mirror of that, and a key that was never set reads as null:
$session = Pop\Session\Session::getInstance();
$userId = $session->user_id ?? null;
$role = $session['role'] ?? 'guest';
if (isset($session->user_id)) {
unset($session->user_id);
}
In a controller that is the whole idiom — fetch, read, write:
<?php
namespace App\Http\Controller;
use Pop\Session\Session;
class CartController extends AbstractController
{
public function add(string $sku): void
{
$session = Session::getInstance();
$items = $session->cart_items ?? [];
$items[] = $sku;
$session->cart_items = $items;
Pop\Http\Server\Response::redirectAndExit('/cart');
}
}
Note the read-modify-write. $session->cart_items[] = $sku does not work: the magic getter returns a copy
rather than a reference, so PHP emits Indirect modification of overloaded property Pop\Session\Session::$cart_items has no effect and the stored array is unchanged. Pull the array out, change
it, put it back.
toArray() returns everything the session holds, count() gives the number of top-level keys, and the object
is iterable, so foreach ($session as $key => $value) walks it. Both hide the framework's own bookkeeping key
(_POP_SESSION_), so what you see is what you put there.
getId() and getName() return the current session id and the session cookie's name (PHPSESSID unless PHP
is configured otherwise). regenerateId() issues a new id and keeps the data — call it on any privilege
change, a successful login above all, and see Authentication for where that fits.
Cookie parameters for the session cookie itself are set once, on the first getInstance() call of the
request, from the options array:
$session = Pop\Session\Session::getInstance([
'lifetime' => 3600,
'path' => '/',
'secure' => true,
'httponly' => true,
'samesite' => 'Strict'
]);
httponly defaults to true and samesite to Lax, which is the safe pair to start from. Options passed on
a later call in the same request are ignored, since the session is already running — set them in load() on
your application class, before anything else touches the session.
Session Namespaces#
A namespace is a named sub-array of the session with its own accessors, which keeps unrelated features from colliding over key names:
$cart = new Pop\Session\SessionNamespace('cart');
$cart->items = ['ORD-1001', 'ORD-1002'];
$cart->currency = 'USD';
print_r($cart->toArray()); // ['items' => [...], 'currency' => 'USD']
Constructing a namespace starts the session if needed, so there's no separate Session::getInstance() call
to remember. Two namespaces cannot see each other's keys, and $cart->user_id is null even when the root
session has a user_id.
The relationship is one-directional, though: a namespace is a key in $_SESSION, so the root session can
see it. Session::getInstance()->cart returns the namespace's whole array. Namespacing organizes your own
keys; it's not an isolation boundary.
kill() on a namespace clears that namespace and leaves the rest of the session intact. kill(true) destroys
the entire session, which is the same thing Session::kill() does.
$cart = new Pop\Session\SessionNamespace('cart');
$cart->kill(); // drops the cart, keeps the login
$cart->kill(true); // destroys the whole session
_POP_SESSION_ is reserved for the component's own bookkeeping and constructing a namespace with that name
throws Pop\Session\Exception — Error: Cannot use the reserved namespace '_POP_SESSION_'.
Values that Expire on Their Own#
Two setters attach a lifetime to a key, and the component removes it once that lifetime is up. Both exist on
Session and on SessionNamespace with the same signatures.
setRequestValue() counts requests. A value set with one hop survives exactly one further request and is gone
on the next — flash messages, in other words:
$session = Pop\Session\Session::getInstance();
$session->setRequestValue('flash', 'Your order was saved.', 1);
$session = Pop\Session\Session::getInstance();
if (isset($session->flash)) {
$view->flash = $session->flash;
}
Set it before a redirect, read it on the page you redirected to, and there's nothing to clean up: the request
after that one no longer has the key, and isset() on it is false.
setTimedValue() counts seconds instead, defaulting to 300:
$session = Pop\Session\Session::getInstance();
$session->setTimedValue('checkout_token', bin2hex(random_bytes(16)), 600);
Expiry is evaluated when the session is next fetched rather than on a timer, so a value whose window has passed sits on disk until the next request touches the session.
| Setter | Unit | Default | Removed when |
|---|---|---|---|
setRequestValue($key, $value, $hops) |
requests | 1 hop | the session has been fetched more than $hops times since |
setTimedValue($key, $value, $expire) |
seconds | 300 | the session is fetched after $expire seconds have passed |
Both write an ordinary session key, so everything else on this page — property access, isset(), toArray() —
works on them unchanged until they vanish.
Cookies#
A cookie lives in the browser where the visitor can read and edit it; session data lives on the server and the cookie carries only the id pointing at it. So anything the application trusts — a user id, a role, a permission — belongs in the session, and cookies hold things a visitor changing them would only affect for themselves, like a theme. A cookie also survives a session's expiry, which is what makes "remember me" a cookie.
Cookie::getInstance() takes the same shape of options array, and unlike the session it accepts new options
at any point in the request, since each cookie is written separately:
$cookie = Pop\Cookie\Cookie::getInstance([
'expires' => time() + 3600,
'path' => '/',
'httponly' => true,
'samesite' => 'Strict'
]);
$cookie->set('theme', 'dark');
$cookie->theme = 'dark'; // the same thing
Reading uses $_COOKIE, so a cookie you write is readable on the next request, not this one. A non-scalar
value is JSON-encoded on the way out and decoded on the way back, which makes a small structure practical to
store:
$cookie = Pop\Cookie\Cookie::getInstance();
$cookie->set('prefs', ['lang' => 'en', 'tz' => 'UTC']);
$prefs = $cookie->prefs; // ['lang' => 'en', 'tz' => 'UTC'] next request
$theme = $cookie->theme ?? 'light';
expires defaults to 0, a browser-session cookie discarded when the browser closes. Pass an absolute Unix
timestamp for anything that should outlive the visit; path defaults to / and the domain to the current
host.
delete($name) expires one cookie, clear() expires every cookie the request carried, and count() and
toArray() report what $_COOKIE holds. getExpires(), getPath(), getDomain(), isSecure(),
isHttpOnly() and getSamesite() read back the options currently in force.
Two validation rules are enforced when options are set: samesite must be one of None, Lax or Strict,
and None requires secure. Either violation throws Pop\Cookie\Exception — Error: The 'samesite' option must be 'None', 'Lax' or 'Strict'. and Error: A 'samesite' value of 'None' requires the 'secure' option to be set to true.
Set cookie options immediately before each set(), and perform deletions last. Options live on the
singleton rather than per call, and delete() and clear() leave the instance's expiry in the past.
delete() also leaves $_COOKIE alone, so isset($cookie->theme) stays true for the rest of the request
that deleted it. The browser is the thing that changed, and it tells you on the next request.
Reading a cookie from inside a controller has a second route: $this->request->getCookie('theme') reads the
same superglobal through the request object. Use whichever reads better where you are; writing always goes
through Pop\Cookie\Cookie — see Requests & Responses.
Ending a Session#
kill() unsets the data, destroys the session, expires the session cookie and drops the singleton, which is
what a logout wants:
$session = Pop\Session\Session::getInstance();
$session->kill();
Everything goes, namespaces included. A getInstance() after a kill() starts a fresh session with a new id.
close() is the other end of the scale: it releases PHP's session lock without ending the session, so a slow
response does not block the same visitor's concurrent requests. Call it once the session data is settled and
the rest of the action is long-running.
Sessions and cookies carry more configuration than this page covers — custom save handlers through
Session::setHandler(), and the full cookie option surface — see the pop-session and pop-cookie READMEs.
See Also#
- Controllers — where session reads and writes usually sit
- Requests & Responses — reading cookies off the request object
- Views & Templates — rendering a flash message into a page
- Authentication — what to put in the session after a login, and when to regenerate the id
- pop-session README — the full session API
- pop-cookie README — the full cookie API