skip to content

In PHP, how do setcookie() and $_COOKIE work together, and why is a cookie you just set missing from $_COOKIE?

level: juniorimportance: must knowfreq 70%

answer

  1. response header vs request header
  2. before any output, or false
  3. visible on the next request
  4. options array since 7.3
  5. delete with the same path and domain

basics

~20 s

setcookie() queues a Set-Cookie response header, so it must run before output; $_COOKIE holds only what the browser sent with the current request. A cookie set now appears in $_COOKIE on the next request, not this one.

solid answer

~40 s

`setcookie()` never touches `$_COOKIE`: it adds a `Set-Cookie` header to the response, so it has to run before any output reaches the client and returns `false` if headers were already sent. `$_COOKIE` is built once at request startup from the `Cookie` header the browser sent, so a cookie set now only shows up on the browser's next request. Modern code uses the options-array form, `setcookie('theme', 'dark', ['expires' => time() + 86400, 'path' => '/', 'secure' => true, 'httponly' => true, 'samesite' => 'Lax'])`; an unknown key throws a `ValueError`, and PHP 8.5 added a `partitioned` key. The value is URL-encoded on the way out and decoded into `$_COOKIE`. To delete a cookie you call `setcookie()` again with the same name, path and domain and an empty value or a past expiry.

code

php · 20 lines
php
<?php
declare(strict_types=1);

// What the browser sent with THIS request
$theme = $_COOKIE['theme'] ?? 'light';

if (isset($_GET['theme'])) {
    $theme = $_GET['theme'] === 'dark' ? 'dark' : 'light';
    $queued = setcookie('theme', $theme, [
        'expires'  => time() + 30 * 86400,
        'path'     => '/',
        'secure'   => true,
        'httponly' => true,
        'samesite' => 'Lax',
    ]);
    // $queued is false only if output was already sent.
    // $_COOKIE['theme'] still holds the value from the request.
}

echo 'Theme: ' . htmlspecialchars($theme);

go deeper

for a junior

Recall that setcookie() writes a response header and $_COOKIE reads the request header, so a new cookie shows up only on the next request, and that setcookie() must run before output.

for a middle

Explain the options array, which keys it accepts, the ValueError and ArgumentCountError it throws, URL encoding versus setrawcookie(), and why a deletion must repeat the path and domain.

for a senior

Show how you track down headers-already-sent bugs that output buffering hides, and how you centralise cookie defaults so no call site forgets secure, httponly or samesite.

for a principal

Weigh one audited cookie-writing helper against scattered setcookie() calls, and decide what may live in a client-editable cookie at all versus server-side state.

## Two directions, two APIs A cookie travels in two different HTTP headers, and PHP gives each direction its own API: - **Reading** uses the `$_COOKIE` superglobal. At request startup PHP parses the `Cookie` request header the browser sent and fills `$_COOKIE` with name-value pairs, URL-decoding each value. - **Writing** uses `setcookie()` (or `setrawcookie()`). It does not change any array; it queues a `Set-Cookie` **response** header that tells the browser to store the cookie. Because the two sides are separate, a script that calls `setcookie('lang', 'fr')` and then reads `$_COOKIE['lang']` still sees whatever the browser sent with *this* request. The new value appears only when the browser makes its next request and includes the cookie in its `Cookie` header. If the rest of the current request needs the new value, keep it in your own variable (or assign `$_COOKIE['lang']` yourself, knowing that is only a local convenience). ## The setcookie() signature and the options array The PHP 8.5 signature is: `setcookie(string $name, string $value = "", array|int $expires_or_options = 0, string $path = "", string $domain = "", bool $secure = false, bool $httponly = false): bool` Since PHP 7.3 the third argument may be an **options array**, which is the readable form and the only one that can set `SameSite`: | Key | Meaning in PHP | Default when omitted | |---|---|---| | `expires` | Unix timestamp, e.g. `time() + 3600`; `0` means a browser-session cookie | `0` | | `path` | URL path the cookie applies to | empty, so the browser uses the current directory | | `domain` | domain the cookie applies to | empty, current host only | | `secure` | send only over HTTPS | `false` | | `httponly` | hide from browser scripts | `false` | | `samesite` | `Lax`, `Strict` or `None` | no attribute sent | | `partitioned` | PHP 8.5: mark as a partitioned (CHIPS) cookie | `false` | What each attribute does in the browser is HTTP cookie semantics; the PHP-specific rules are about how the function validates its input: 1. With an array as the third argument, `setcookie()` accepts **exactly three arguments**; a fourth throws an `ArgumentCountError`. 2. A key outside the list throws a `ValueError` naming the invalid option, and numeric keys are rejected too. 3. `partitioned => true` without `secure => true` throws a `ValueError`. 4. An empty name, or a name containing `=`, `,`, `;`, space or control whitespace, throws a `ValueError`; so does an `expires` year above 9999. `setcookie()` encodes the value with raw URL encoding, so spaces and semicolons survive. `setrawcookie()` has the same parameters but sends the value untouched and rejects values containing those characters. ## Headers before body `Set-Cookie` is a header, and headers go out before the first byte of the body. Once output has actually been sent, `setcookie()` emits the warning "Cannot modify header information - headers already sent by (output started at file:line)" and returns `false`. The usual culprits are an `echo` before the call, whitespace after a closing `?>` in an included file, or a byte-order mark at the top of a file. Output buffering can hide the problem in one environment and expose it in another, because buffered output has not been *sent* yet. A `true` return only means the header was queued. It says nothing about whether the browser accepted or stored the cookie. ## Deleting or replacing a cookie There is no delete function. You send the cookie again: - **Replace** it by calling `setcookie()` with the same name, path and domain and a new value. - **Delete** it with the same name, path and domain and an empty value; PHP then sends the value `deleted` with an expiry in 1970 and `Max-Age=0`. An explicit past `expires` works too. The browser matches cookies by name, path and domain together. A deletion sent with a different path creates a second cookie instead of removing the first, which is the classic reason a logout cookie seems to come back. ## Traps worth naming in an interview - Expecting `$_COOKIE` to reflect a `setcookie()` call in the same request. - Calling `setcookie()` after output and ignoring the `false` return. - Storing a secret or a user ID in plain cookie text; anything in `$_COOKIE` came from the client and can be edited. - Mixing the positional and array styles in one call. - Forgetting that cookie values arrive as strings (or nested arrays for bracketed names such as `pref[a]`), never as integers: `$_COOKIE['count']` gives `"3"`, not `3`.

  • What happens if you pass the options array and also a fourth argument to setcookie()?
    PHP throws an `ArgumentCountError`: when the third argument is an array, `setcookie()` expects exactly three arguments. The array replaces the positional path, domain, secure and httponly parameters, so mixing styles is rejected rather than merged. A key outside `expires`, `path`, `domain`, `secure`, `httponly`, `samesite` and `partitioned` throws a `ValueError` naming the invalid option.
  • What is the difference between setcookie() and setrawcookie() in PHP?
    They take the same parameters and emit the same header. `setcookie()` URL-encodes the value, so spaces and semicolons survive the trip; `setrawcookie()` sends it as given and throws a `ValueError` if it contains a comma, semicolon, space or control whitespace. Either way, `$_COOKIE` gives you URL-decoded values on the next request.
  • In PHP 8.5, what does the partitioned option of setcookie() require?
    It must be paired with `'secure' => true`; `setcookie()` throws a `ValueError` if `partitioned` is set without `secure`. The same key is accepted by `session_set_cookie_params()` and `session_start()` in PHP 8.5, and the session module warns instead of throwing when its partitioned setting is on without the secure one.

saying these in an interview costs you the question

  • setcookie() adds the new cookie to $_COOKIE immediately
  • setcookie() still works after output has been sent to the client
  • A true return means the browser accepted and stored the cookie
  • Deleting a cookie only needs its name, not the original path
  • The options array accepts any cookie attribute name as a key