Cookies
A cookie is a small piece of data the server asks the browser to store and send back with every later request to the same site. Sessions use a cookie for their ID; you can also set your own cookies for preferences like a theme or language. Because cookies live on the visitor's computer, they need care: users can read and change them.
Setting and reading a cookie
setcookie() sends a Set-Cookie header. Like all headers, it must be called before any output. On later requests, the value appears in $_COOKIE:
<?php $allowed = ['light', 'dark']; if (isset($_GET['theme']) && in_array($_GET['theme'], $allowed, true)) { setcookie('theme', $_GET['theme'], [ 'expires' => time() + 60 * 60 * 24 * 365, // one year 'path' => '/', 'httponly' => true, 'samesite' => 'Lax', ]); $_COOKIE['theme'] = $_GET['theme']; // so this request sees the new value too } $theme = in_array($_COOKIE['theme'] ?? '', $allowed, true) ? $_COOKIE['theme'] : 'light'; ?> <body class="<?= $theme ?>">Current theme: <?= $theme ?></body>
GET /theme.php GET /theme.php?theme=dark GET /theme.php
> GET /theme.php <body class="light">Current theme: light</body> > GET /theme.php?theme=dark <body class="dark">Current theme: dark</body> > GET /theme.php <body class="dark">Current theme: dark</body>
The third request has no ?theme= in the URL, yet it's dark: the browser sent the cookie back. Note that $_COOKIE only contains what the browser sent with the current request, which is why the code also updates it by hand right after setting it.
What the header looks like
<?php setcookie('lang', 'pt', [ 'path' => '/', 'secure' => true, 'httponly' => true, 'samesite' => 'Strict', ]); setcookie('visited', 'yes'); echo "Cookies set";
GET /set.php
> GET /set.php < Set-Cookie: lang=pt; path=/; secure; HttpOnly; SameSite=Strict < Set-Cookie: visited=yes Cookies set
| Option | Meaning |
|---|---|
expires | Unix timestamp when the cookie expires. Without it, it's a "session cookie" deleted when the browser closes. |
path, domain | Which URLs the browser sends it to. Use / for the whole site. |
secure | Only sent over HTTPS. Turn on for every production site. |
httponly | Not readable by JavaScript, which limits the damage of an XSS bug. |
samesite | Lax, Strict or None: whether the cookie is sent with requests from other sites. Helps against CSRF (lesson 59). |
With an expires option, the header also gets expires=… and Max-Age=… attributes.
Deleting a cookie
To delete a cookie, set it again with an expiry date in the past, using the same path and domain:
<?php setcookie('theme', '', ['expires' => 1, 'path' => '/']); echo "Theme preference removed";
GET /forget.php
> GET /forget.php < Set-Cookie: theme=deleted; expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/ Theme preference removed
Never trust cookie contents
Users can edit their cookies freely with browser developer tools. Treat $_COOKIE exactly like form input — validate it, and never store anything in a cookie that grants permissions:
<?php // WRONG: trusting a cookie for authorisation if (($_COOKIE['is_admin'] ?? '') === '1') { echo "Welcome to the admin panel (anyone can get here!)"; } else { echo "Access denied"; }
GET /admin.php
> GET /admin.php Access denied
Anyone could set is_admin=1 in their browser and get in. Store the user's ID in a session (on the server) and look up their permissions there. If you need tamper-proof data in a cookie itself, sign it with hash_hmac and a secret key, and verify the signature with hash_equals when reading it back.
Cookies and privacy
Cookies used for tracking or analytics may require the visitor's consent under laws like the EU's GDPR and ePrivacy rules. Cookies strictly necessary for the site to work — a session for login or a shopping cart — generally don't. Keep cookies few and small: they're sent with every single request.
Try it yourself
Remember the visitor's preferred language in a cookie and greet them in it. Only accept languages from a fixed list.