-
-
Notifications
You must be signed in to change notification settings - Fork 0
Recipe Consent Cookie
Store a long-lived cookie-consent / preference flag, read it to gate analytics or other optional features, and let the user withdraw consent. The flag is not secret — only whether the user agreed — so a signed (not encrypted) cookie is a good fit: the signature stops a client from silently flipping the flag.
A consent flag is a low-stakes, readable value. The HMAC signature still
earns its keep: it makes the stored decision tamper-evident, so the value your
server reads back is the one your server wrote, not one a client edited. The
client can see consent=true — that is fine; what matters is it cannot forge
a different decision that your code then trusts. See the
Security Model.
Use a dedicated, long-lived cookie. A consent decision typically lasts a year,
so set both the transport ttl and the per-key $ttl to a year:
use InitPHP\Cookies\Cookie;
function recordConsent(bool $analytics): void
{
$oneYear = 365 * 86400;
$cookie = new Cookie('consent', getenv('COOKIE_SALT'), [
'ttl' => $oneYear,
'secure' => true,
'httponly' => false, // a banner script may need to read it; see the note below
'samesite' => 'Lax',
]);
// Store the decision. A bool round-trips as a real bool.
$cookie->set('analytics', $analytics, $oneYear);
$cookie->set('decided_at', time(), $oneYear);
$cookie->send(); // before any output
}
// From the banner's "Accept" / "Reject" handler:
recordConsent(true); // user accepted analytics
recordConsent(false); // user declined
httponlyand consent banners. The default ishttponly => true, which hides the cookie from JavaScript. If a client-side banner needs to read the decision to decide whether to render, sethttponly => falseas above. Because the value is non-sensitive (a yes/no flag), exposing it to JavaScript is an acceptable trade-off. Keephttponly => trueif only your server reads the flag. See Configuration.
Use has() to tell "no decision yet" from a stored
decision, and get() to read the boolean. Remember that
get('analytics') returns false both when the user declined and when the key
is absent — so check has() first if "undecided" matters:
use InitPHP\Cookies\Cookie;
function analyticsAllowed(): bool
{
$cookie = new Cookie('consent', getenv('COOKIE_SALT'), [
'secure' => true,
'httponly' => false,
'samesite' => 'Lax',
]);
// No decision recorded yet → treat as "not allowed" and show the banner.
if (!$cookie->has('analytics')) {
return false;
}
return (bool) $cookie->get('analytics');
}
if (analyticsAllowed()) {
// load analytics tags / write tracking cookies
} else {
// render the consent banner, or simply do nothing
}| State | has('analytics') |
get('analytics') |
Meaning |
|---|---|---|---|
| Cookie absent / expired | false |
null (or your default) |
No decision — show the banner |
| User accepted | true |
true |
Analytics allowed |
| User declined | true |
false |
Analytics not allowed |
Two options, depending on whether you want the cookie to linger as an explicit "declined" record or to disappear entirely.
Overwrite the decision so a later read sees analytics => false:
recordConsent(false); // writes analytics => false, keeps the cookieThis is usually the better choice: the cookie now positively records "the user declined", which prevents the banner from reappearing on every page.
If withdrawal should mean "forget my choice and ask again", remove the keys (or delete the whole cookie) and send:
use InitPHP\Cookies\Cookie;
$cookie = new Cookie('consent', getenv('COOKIE_SALT'), [
'secure' => true,
'httponly' => false,
'samesite' => 'Lax',
]);
// Remove the specific keys...
$cookie->remove('analytics', 'decided_at');
$cookie->send();
// ...or delete the cookie outright (matching the issuing path/domain):
$cookie->destroy();
$cookie->send(); // no-op after destroy(), but harmlessAfter Option B, has('analytics') is false again, so your gate treats the
user as undecided and the banner returns. See
Reading & Removing for remove vs.
destroy.
-
Reading
get('analytics')withouthas(). A declined user and an undecided user both yieldfalse. Usehas()when "undecided" must trigger the banner. -
A per-key TTL longer than the transport
ttl. If the cookie's transportttlis shorter than your per-key year, the browser drops the whole cookie first and the decision is lost early. Set both to the same lifetime. See TTL & Expiry. - Storing personal data in the consent cookie. Keep it to the decision flags and a timestamp. The cookie is readable; do not put anything sensitive in it. See the Security Model.
-
Forgetting
send(). Recording and withdrawing both require asend()before output. See Sending & Lifecycle.
-
Basic Usage —
has()vs.get()for booleans. - TTL & Expiry — matching the per-key and transport lifetimes.
-
Configuration —
secure,httponly,samesitefor this cookie. -
Reading & Removing —
removevs.destroyfor withdrawal.
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