-
-
Notifications
You must be signed in to change notification settings - Fork 0
Sending and Lifecycle
Understand when data actually reaches the browser, why send() must run
before output, the no-op semantics that make send() cheap to call, and why
the destructor safety-net is not a substitute for an explicit send().
Every mutating method — set, setArray, push, remove, flush —
changes only an in-memory working copy and sets an internal "changed" flag.
None of them touch the browser:
use InitPHP\Cookies\Cookie;
$cookie = new Cookie('app_session', getenv('COOKIE_SALT'));
$cookie->set('user_id', 42); // staged, nothing written yet
$cookie->set('theme', 'dark'); // staged
// ... still nothing on the wire ...A single send() then serializes the non-expired
working copy, signs it, and hands it to the writer (native setcookie() by
default):
$cookie->send(); // one Set-Cookie header for 'app_session'Because every value lives in one signed cookie, you pay for exactly one
Set-Cookie header no matter how many values you staged.
send() short-circuits and returns true without writing when nothing has
changed since the last send. This makes it safe to call defensively, and
idempotent until the next mutation:
$cookie = new Cookie('app_session', getenv('COOKIE_SALT'));
$cookie->send(); // no mutation occurred → no-op, returns true, writes nothing
$cookie->set('a', '1');
$cookie->send(); // writes once
$cookie->send(); // no-op — nothing changed since the previous sendLoading a valid, unchanged incoming cookie does not flag a change, so a
read-only request issues no Set-Cookie at all. A tampered or malformed
incoming cookie does flag a change, so a clean replacement is re-issued —
see the Security Model.
send() returns the writer's success boolean. With the default native writer
that reflects whether setcookie() returned true. Be aware of what the
boolean does not distinguish:
- A no-op (nothing changed) returns
true— identical to a successful write. - A late write —
send()called after output — typically still reportstruefrom the writer's perspective even though the header was dropped, because the default writer suppresses the "headers already sent" warning (see below).
In other words, a true from send() means "the writer did not report a
hard failure", not "the cookie definitely reached the browser". The reliable
guarantee comes from calling send() before any output, not from
inspecting its return value.
send() writes an HTTP response header. Like PHP's native setcookie(), it
only works before any output has been sent — no echo, no rendered HTML,
no whitespace or BOM outside the PHP tags. Once the body has started, the
header is dropped.
The recommended pattern is to finish all cookie mutations and call send()
at the end of request handling, before you render:
// 1. Handle the request, mutate cookies as needed.
$cookie->set('last_seen', time());
// 2. Flush cookies to headers.
$cookie->send();
// 3. Only now produce output.
echo $renderedHtml;In a PSR-7 / middleware stack, run send() (or your own writer) while you
still control the headers, before the response body is emitted.
When the Cookie instance is destroyed (end of request, or when it goes out
of scope), its destructor calls send() once. This is a safety-net so
pending changes are not silently lost if you forget to flush:
function handle(): void
{
$cookie = new Cookie('app_session', getenv('COOKIE_SALT'));
$cookie->set('a', '1');
// No explicit send(). When $cookie is destroyed at the end of the
// function, the destructor calls send() for you.
}If you already called send() and nothing changed afterward, the
destructor's send() is a no-op, so there is no double write.
- Output may already be flushed by then. The destructor often runs during shutdown, after your response body has been sent. At that point the header is too late and the write is lost.
-
The native writer suppresses the warning. The default writer prefixes
setcookie()with@, specifically so the destructor cannot emit a "headers already sent" warning during shutdown. That means a too-late write fails silently — you would not see an error, only a missing cookie.
Treat the destructor as a seatbelt, not a strategy. Call send() explicitly
while you still control the headers.
construct ── decode + verify incoming cookie ──► working copy
│
├─ set / setArray / push / remove / flush ──► mutate working copy, mark changed
│
├─ send() ──► if changed: serialize → sign → write; else no-op
│
├─ destroy() ──► write deletion cookie now, clear working copy, send() becomes a no-op
│
└─ __destruct() ──► send() safety-net (no-op if already sent)
-
send()after output. The single most common cause of "my cookie isn't set". Movesend()before anyecho/render. See the FAQ. -
Leaning on the destructor. It can fire after output, where the write
silently fails. Call
send()explicitly. -
Expecting a write on a read-only request. If you only read a valid
cookie, nothing changed, so
send()is a no-op by design. -
Trusting
send()'strueas proof of delivery. It only means the writer did not hard-fail. Call it before output to be certain.
-
Reading & Removing —
flushvs.destroylifecycle differences. - Security Model — why a tampered cookie triggers a re-issue.
-
Testing — inject a recording writer to assert what
send()emitted. -
API Reference — the
sendsignature and contract.
initphp/cookies · MIT License · part of the InitPHP family
Source · Issues · Discussions · Packagist · Contributing · Security Policy
Getting Started
Core Usage
Reference
Practical Guides
Migration & Help