Almost always because send() ran after output. Like native
setcookie(), send() writes an HTTP header, which only works before
any body has been emitted (no echo, no HTML, no whitespace/BOM
outside the PHP tags). Call send() at the end of request handling,
before you render.
$cookie->set('a', '1');
$cookie->send(); // BEFORE any output
echo $html; // output starts hereDo not rely on the destructor: it often runs during shutdown, after the body has flushed, and the default writer suppresses the "headers already sent" warning — so a too-late write fails silently. See Sending and lifecycle.
The usual cause is a per-key TTL that has elapsed. The $ttl argument
of set()/setArray()/push() is seconds from now; once it passes,
the entry is removed on the next read and never re-sent. Check:
- You did not pass a short
$ttlyou have since forgotten about. - The per-key TTL is not longer than the transport
ttloption — if it is, the browser drops the whole cookie when the transport TTL elapses. - You actually called
send()after the mutation.
See TTL and expiry.
No, that is by design. The salt is the HMAC key. When it changes, every previously issued cookie fails the signature check and is silently replaced with an empty (freshly signed) cookie. Users lose their stored values. Rotate the salt only when you intend exactly that — it doubles as a global "invalidate every cookie" switch. See the security model.
Yes. The payload is signed, not encrypted — it is base64 of a plain PHP serialization, which anyone can decode. The signature proves the data was issued by you and detects tampering; it does not hide the values. Do not store secrets, password hashes, or sensitive PII. Store an opaque reference and keep the real data server-side. See the security model and the remember-me recipe.
Only string, bool, int, float and numeric strings are accepted;
anything else throws CookieInvalidArgumentException. To store
structured data, encode it into a string yourself and decode on read:
$cookie->set('prefs', json_encode(['theme' => 'dark', 'lang' => 'en']));
// ...later...
$prefs = json_decode((string) $cookie->get('prefs', '{}'), true);Keep the total payload small (see the next entry).
Yes. Browsers cap a single cookie at roughly 4 KB (4096 bytes) for the whole name=value pair, and the value here also includes the base64 overhead plus a 64-character HMAC signature. Because every value you set shares one cookie, a large value or many values can blow past the limit, at which point the browser silently drops the cookie. Keep the combined payload well under 4 KB; store bulk data server-side.
null is not a storable value type, so set('k', null) throws. But
get() returns null as its default when a key is absent. To remove
a value, call remove('k') rather than setting it to null. To tell
"absent" from a stored false/0/'', use has().
flush()clears the values but keeps the cookie: the nextsend()writes an empty, still-signed cookie that stays in the browser.destroy()writes a deletion cookie immediately, telling the browser to remove it, and makes the nextsend()a no-op.
Use flush() to empty the contents, destroy() to delete the cookie
(e.g. logout). See
Reading and removing.
Cookie is final and its properties are private to keep the
security-sensitive surface (signing, verification, the deserialization
guard) closed to accidental override. A subclass that touched encoding,
the salt, or the verification flow could quietly defeat the tamper
protection. Extend by composition: depend on CookieInterface and
wrap a Cookie instance in your own class, as the
flash-messages and
testing recipes do.
It reads $_COOKIE by default (you can inject $source instead) and
never writes to it. Writes go through the injectable $writer, which
defaults to native setcookie(). This makes the class fully testable
without superglobal or header juggling — see the
testing recipe.
PHP 7.4 through 8.4. The 2.0 line requires 7.4+ (1.x advertised 7.2 but
already relied on the PHP 7.3 setcookie() options-array signature).
See Upgrading from 1.x.