initphp / cookies
Signed, tamper-evident cookie manager for PHP with per-key TTL support.
Requires
- php: ^7.4 || ^8.0
- initphp/parameterbag: ^2.0
Requires (Dev)
- friendsofphp/php-cs-fixer: ^3.64
- phpstan/phpstan: ^1.12
- phpunit/phpunit: ^9.6
README
A signed, tamper-evident cookie manager for PHP. Values are stored in a single browser cookie whose payload is authenticated with an HMAC-SHA256 signature, so a client cannot read-tamper its way into forging cookie data. Each value can carry its own time-to-live.
Features
- Signed payload — the whole cookie is signed with HMAC-SHA256 and
verified in constant time (
hash_equals). A tampered cookie is rejected and transparently re-issued clean. - Hardened against object injection — deserialization runs only
after the signature check and forbids object instantiation
(
allowed_classes => false). - Per-key TTL — every value may expire independently; expired values are dropped on read and never re-sent.
- One browser cookie, many values — group related values under a single named, signed cookie.
- Deferred writes — mutations are staged in memory and flushed with
a single
send()(or by the destructor as a safety-net). - Testable by design — the raw cookie source and the low-level writer are injectable, so no superglobal or header juggling is needed in tests.
Requirements
- PHP 7.4, 8.0, 8.1, 8.2, 8.3 or 8.4
- initphp/parameterbag
^2.0
Installation
composer require initphp/cookies
Quick start
require_once __DIR__ . '/vendor/autoload.php'; use InitPHP\Cookies\Cookie; // The salt is the HMAC secret. Keep it private and stable across requests. $cookie = new Cookie('app_session', getenv('COOKIE_SALT')); $cookie->set('user_id', 42); $cookie->set('flash', 'Saved!', 60); // expires in 60 seconds // Flush the staged changes to the browser before any output is sent. $cookie->send();
On the next request:
$cookie = new Cookie('app_session', getenv('COOKIE_SALT')); $cookie->has('user_id'); // true $cookie->get('user_id'); // 42 (int — scalar types are preserved) $cookie->get('missing', '-'); // '-' (default) $cookie->pull('flash'); // reads the value once, then removes it
Important: like PHP's native
setcookie(),send()writes HTTP headers, so it must run before any output. Call it explicitly at the end of your request handling. The destructor callssend()as a safety-net, but relying on it is discouraged.
How it works
The manager keeps an in-memory working copy of your values. Mutating
methods change only that copy; nothing reaches the browser until
send() is called. On send() the working copy is:
- stripped of expired entries,
serialize()-d and base64-encoded into apayload,- signed:
signature = hash_hmac('sha256', payload, salt), - written as the cookie value
"{payload}.{signature}".
On construction the incoming cookie is verified before anything is
deserialized: the signature is recomputed and compared with
hash_equals(). If it does not match (tampering, a different salt, a
truncated value), the payload is discarded and a clean cookie is
re-issued. Because deserialization happens only after that check and
with allowed_classes => false, a malicious cookie cannot trigger PHP
object injection.
Configuration
The third constructor argument overrides the default cookie attributes. Only the keys you pass are changed; the rest keep their defaults.
$cookie = new Cookie('app_session', $salt, [ 'ttl' => 2592000, // transport-cookie lifetime in seconds (30 days) 'path' => '/', 'domain' => null, 'secure' => false, 'httponly' => true, 'samesite' => 'Strict', // 'Strict' | 'Lax' | 'None' ]);
| Option | Type | Default | Description |
|---|---|---|---|
ttl |
int |
2592000 |
Lifetime of the browser cookie in seconds. Per-value TTLs are separate. |
path |
string |
'/' |
Cookie path. |
domain |
string|null |
null |
Cookie domain. Omitted from the header when null. |
secure |
bool |
false |
Send only over HTTPS. |
httponly |
bool |
true |
Hide the cookie from JavaScript. |
samesite |
string |
'Strict' |
Strict, Lax or None. None automatically forces secure => true. |
ttl vs. per-key TTL
- The
ttloption controls how long the browser keeps the single transport cookie. - The
$ttlargument ofset()/setArray()/push()controls how long an individual value is considered valid by the manager. Anullper-key TTL means "live as long as the transport cookie".
API
public function has(string $key): bool; public function get(string $key, mixed $default = null): mixed; public function pull(string $key, mixed $default = null): mixed; public function set(string $key, string|int|float|bool $value, ?int $ttl = null): self; public function setArray(array $assoc, ?int $ttl = null): self; public function push(string $key, string|int|float|bool $value, ?int $ttl = null): mixed; public function all(): array; public function remove(string ...$key): self; public function send(): bool; public function flush(): bool; public function destroy(): bool;
| Method | Description |
|---|---|
has |
Whether a non-expired value exists. An expired value is removed and reported as absent. |
get |
The value for $key, or $default when absent/expired. Scalar types are preserved. |
pull |
Like get, but removes the value afterwards (read-once). |
set |
Stage a single value. $ttl is seconds from now, or null for no per-key expiry. |
setArray |
Stage several values from an associative array sharing one TTL. |
push |
Like set, but returns the staged value. |
all |
All non-expired values as a key => value map. |
remove |
Stage removal of one or more keys. |
send |
Write the staged state to the browser. No-op when nothing changed. |
flush |
Empty all values; the next send() writes an empty (still signed) cookie. |
destroy |
Immediately expire and clear the transport cookie in the browser. |
Allowed value types are string, bool, int, float and numeric
strings. Anything else throws
InitPHP\Cookies\Exception\CookieInvalidArgumentException.
Documentation
Full developer documentation lives in docs/:
getting started, usage guides, the configuration reference, the security
model, the API reference and practical recipes.
Upgrading from 1.x
Version 2.0 is a security and correctness release with intentional breaking changes:
- Cookie format changed. The payload is now signed with HMAC-SHA256 (was MD5) and the envelope layout is different, so cookies issued by 1.x are not readable by 2.x — clients simply receive a fresh cookie. The previous 1.x behaviour also made any value set with an explicit TTL unreadable; 2.0 fixes that.
- PHP 7.4+ is now required (1.x advertised 7.2, but already relied on
the PHP 7.3
setcookie()options array). Cookieisfinaland its properties areprivate. Extend by composition rather than inheritance.newoptional constructor arguments —$source(raw cookie array) and$writer(low-level writer) were added after$optionsfor testability; existing 3-argument calls are unaffected.
Testing
composer install composer test # PHPUnit composer analyse # PHPStan (level 8) composer cs:check # PHP-CS-Fixer (dry-run)
Credits
License
Released under the MIT License. Copyright © 2022 InitPHP.