$_COOKIE
What Are Cookies?
In simple terms: A cookie is a small piece of text that a website asks the browser to store. On every later request to that website, the browser sends the cookie back. This lets a site "remember" things between page visits — a preferred language, a theme, or which user is logged in. Technically: A server sets a cookie with a Set-Cookie HTTP response header. The browser stores it and includes it in the Cookie request header of subsequent matching requests. PHP parses that header into the $_COOKIE superglobal. Cookies are set with setcookie().
Why Cookies Exist
HTTP is stateless: each request is independent, and the server does not automatically know that two requests came from the same visitor. Cookies add a small amount of state on the client side. Sessions (see $_SESSION) build on cookies: the browser stores only a session ID cookie, and the actual data stays on the server.
The Cookie Lifecycle
Request 1: Browser ──GET /settings.php──────────────────────────► Server
Browser ◄──Set-Cookie: theme=dark; Max-Age=31536000── Server (setcookie)
Request 2: Browser ──GET /index.php Cookie: theme=dark─────────► Server
$_COOKIE['theme'] === 'dark'
Important: A cookie set with
setcookie()is not available in$_COOKIEduring the same request.$_COOKIEreflects what the browser sent with the current request; the new cookie arrives with the next request.
Setting a Cookie
Syntax (PHP 7.3+)
setcookie(string $name, string $value = "", array $options = []): bool
Option Meaning Recommended expiresUnix timestamp when the cookie expires; 0= session cookie (deleted when the browser closes)Set explicitly pathURL path where the cookie is sent '/'domainDomain (and subdomains) where the cookie is sent Usually leave empty (current host only) secureSend only over HTTPS truein productionhttponlyHide from JavaScript ( document.cookie)trueunless JS must read itsamesiteLax,Strict, orNone— controls sending with cross-site requests'Lax'Example
<?php // settings.php — must run before any output is sent setcookie('theme', 'dark', [ 'expires' => time() + 60 * 60 * 24 * 365, // one year 'path' => '/', 'secure' => true, 'httponly' => true, 'samesite' => 'Lax', ]); echo "Theme saved. It will be applied on your next page view.";Response Header Sent to the Browser (date depends on the current time)
Set-Cookie: theme=dark; expires=Mon, 11 Oct 2027 18:00:00 GMT; Max-Age=31536000; path=/; secure; HttpOnly; SameSite=LaxWhy These Options
secureprevents the cookie from travelling over unencrypted HTTP, where it could be intercepted. (Onhttp://localhostduring development, browsers may ignoresecurecookies — set it based on the environment.)httponlystops JavaScript from reading the cookie, which limits the damage of an XSS vulnerability.samesite: Laxprevents the cookie from being sent with most cross-site requests (such as form posts from other sites), which helps against CSRF.Strictis stricter;None(which requiressecure) is needed only for genuinely cross-site use, such as embedded widgets.
Reading Cookies
<?php // Request header: Cookie: theme=dark; lang=ta $allowedThemes = ['light', 'dark']; $theme = $_COOKIE['theme'] ?? 'light'; if (!in_array($theme, $allowedThemes, true)) { $theme = 'light'; } $language = $_COOKIE['lang'] ?? 'en'; echo "Theme: $theme, language: $language\n";Output
Theme: dark, language: taCookies are user input: the user (or an attacker) can edit, delete, or invent any cookie. Read them with defaults and validate them exactly like
$_GETand$_POSTdata.
Headers Must Be Sent Before Output
Cookies travel in HTTP headers, and headers are sent before the page content. Once any output has been sent — even a space or a blank line before
<?php— it is too late:<html> <?php setcookie('theme', 'dark');Result
PHP Warning: Cannot modify header information - headers already sent by (output started at /path/settings.php:1) in /path/settings.php on line 3Fix: Call
setcookie()before printing anything — typically at the top of the script or in a controller, before templates are rendered. The warning message tells you exactly where output started. A common hidden cause is whitespace after a closing?>in an included file, which is why pure PHP files should omit the closing tag.
Deleting a Cookie
There is no "delete" function. Instead, set the same cookie again with an expiry date in the past:
<?php setcookie('theme', '', [ 'expires' => time() - 3600, 'path' => '/', 'secure' => true, 'httponly' => true, 'samesite' => 'Lax', ]); unset($_COOKIE['theme']); // also remove it from the current request's dataThe
pathanddomainmust match the values used when the cookie was set; otherwise the browser treats it as a different cookie and keeps the original.
Storing Structured Data
Cookie values are strings. To store a small list or settings object, encode it as JSON:
<?php function addRecentlyViewed(array $ids, int $productId, int $max = 5): array { $ids = array_values(array_unique([$productId, ...$ids])); return array_slice($ids, 0, $max); } $recent = []; foreach ([12, 7, 12, 3, 9, 4, 15] as $viewedProduct) { $recent = addRecentlyViewed($recent, $viewedProduct); } echo json_encode($recent) . "\n"; // In the real page: // setcookie('recent', json_encode($recent), ['expires' => time() + 2592000, 'path' => '/', 'httponly' => true, 'samesite' => 'Lax']);Output
[15,4,9,3,12]How It Works
- Each viewed product is placed at the front of the list;
array_unique()removes an earlier occurrence, so viewing product 12 again moves it to the front instead of duplicating it.array_slice()keeps only the five most recent IDs, so the cookie stays small. Product 7 dropped off the end.- The list is stored as JSON in the cookie.
Reading It Back Safely
<?php // Request header: Cookie: recent=%5B15%2C4%2C9%2C3%2C12%5D $decoded = json_decode($_COOKIE['recent'] ?? '[]', true); $recent = is_array($decoded) ? array_values(array_filter($decoded, fn($id): bool => is_int($id) && $id > 0)) : []; echo implode(', ', $recent) . "\n";Output
15, 4, 9, 3, 12
setcookie()URL-encodes the value, and PHP decodes it automatically when filling$_COOKIE. Because the cookie might have been edited, the decoded data is checked: it must be an array of positive integers. Never pass cookie contents tounserialize()— it can be exploited to create arbitrary objects.
Protecting Cookies From Tampering
Users can change cookie values freely. If a cookie's value must not be altered, sign it with a secret key using HMAC, and verify the signature when reading:
<?php const COOKIE_SECRET = 'replace-with-a-long-random-secret-from-configuration'; function signValue(string $value): string { return $value . '.' . hash_hmac('sha256', $value, COOKIE_SECRET); } function verifySignedValue(string $signed): ?string { $position = strrpos($signed, '.'); if ($position === false) { return null; } $value = substr($signed, 0, $position); $signature = substr($signed, $position + 1); return hash_equals(hash_hmac('sha256', $value, COOKIE_SECRET), $signature) ? $value : null; } $cookie = signValue('dark'); var_dump(verifySignedValue($cookie)); var_dump(verifySignedValue('light.' . substr($cookie, 5))); var_dump(verifySignedValue('dark'));Output
string(4) "dark" NULL NULLHow It Works
hash_hmac()computes a signature from the value and a secret key that only the server knows.- The cookie stores
value.signature.- When reading, the server recomputes the signature for the value. If the user changed the value (here,
darktolight) without knowing the secret, the signatures no longer match.hash_equals()compares the signatures in constant time, which prevents timing attacks. Signing prevents modification, but the value is still readable. Sensitive data does not belong in cookies at all — store it in the session or database.
Cookie Limits and Rules
Rule Detail Size About 4 KB per cookie (name + value + attributes) Number Browsers limit the number of cookies per domain (commonly 50 or more) Sent with every request Large cookies slow down every request, including images and CSS on the same domain __Host-name prefixBrowsers accept the cookie only if it is secure, haspath=/, and has nodomain— a strong protection for important cookiesPrivacy laws Non-essential cookies (analytics, advertising) may require user consent, depending on the jurisdiction
Common Mistakes
Expecting the Cookie in the Same Request
Effect:
$_COOKIE['theme']is not set right aftersetcookie(). Fix: Use the value you just set directly, or redirect so the next request carries the cookie.Calling
setcookie()After OutputEffect: "Headers already sent" warning, and the cookie is not set. Fix: Set cookies before any output; omit closing
?>in pure PHP files.Storing Sensitive Data in Cookies
Effect: User IDs, roles, or prices can be read or changed by the user. Fix: Store an opaque session ID only; keep data on the server.
Forgetting Security Options
Effect: Cookies readable by JavaScript or sent over HTTP. Fix: Use
secure,httponly, andsamesite.Deleting With Different
path/domainEffect: The cookie is not removed. Fix: Use the same attributes as when setting it.
Best Practices
- Use the options array with
secure,httponly, andsamesitefor every cookie.- Treat
$_COOKIEas untrusted input: use defaults, allow-lists, and type checks.- Keep cookies small; store complex data server-side.
- Sign cookies whose values must not be modified, and never put secrets in them.
- Set cookies before any output, and remember they appear in
$_COOKIEonly on the next request. Official reference: https://www.php.net/manual/en/reserved.variables.cookies.php