๐Ÿ“ Web Development

PHP Cookies: setcookie, $_COOKIE and Expiry

Sep 11, 20265 min read0 viewsBy Flow

A visitor ticks "remember me", comes back the next morning, and the site has forgotten them. The dark theme they picked resets on the next click. HTTP remembers nothing between requests, so you need somewhere to park a scrap of text. PHP cookies are that somewhere: a name, a value and a date, handed back to you on every request after.

The part that catches people is timing. setcookie() does not write to a variable you can read. It adds a header to a response you have not finished sending, so a cookie you set on line three is not in $_COOKIE on line four. Get that straight and most cookie bugs explain themselves.

What PHP cookies actually are

A cookie is not a PHP feature. It is a pair of HTTP headers. Your script sends Set-Cookie: username=Flow, the browser writes it down, and on every later request it sends Cookie: username=Flow back. PHP unpacks that incoming header into $_COOKIE before your first line runs.

So setcookie() is a header-writing function, nothing more. This page sets a cookie, asks whether $_COOKIE knows about it, then prints the headers about to be sent.

<?php
setcookie('username', 'Flow', time() + 3600);

// on this request the cookie is not here yet
var_dump(isset($_COOKIE['username']));

// but the instruction to store it is already in the response
print_r(headers_list());

Output:

php cookies output showing bool false and a Set-Cookie header in the response header list

bool(false), because the browser sent no Cookie header at all. It had nothing to send yet. And a Set-Cookie line waiting in the outgoing headers, which is the instruction the browser is about to obey. The cookie is a promise, not data.

Reading PHP cookies with $_COOKIE

$_COOKIE is an ordinary superglobal array, built once at the start of the request from the header the browser sent. Keys are cookie names, values are always strings, even when you set a number. Load a second page and the value is finally there.

<?php
if (isset($_COOKIE['username'])) {
    echo 'Welcome back, ' . htmlspecialchars($_COOKIE['username']) . "\n";
} else {
    echo "No username cookie came with this request\n";
}

print_r($_COOKIE);

Output:

php cookies output showing the stored username printed from the $_COOKIE array on a second page

Note the htmlspecialchars(). Everything in $_COOKIE came from the browser, so it is user input, the same as a form field. Reach for isset() or $_COOKIE['username'] ?? 'guest' rather than the bare key: reading an absent key raises a warning.

setcookie() must come before any output

PHP cookies are headers, and headers travel ahead of the body. Once one byte of body has gone โ€” an echo, a line of HTML above the opening tag, a blank line after a closing ?> in an include โ€” the headers went with it.

<?php
echo "Setting a cookie...\n";   // this line sends the body

setcookie('theme', 'dark', time() + 3600);

echo "Did it work?\n";

Output:

php cookies output showing the Cannot modify header information warning after HTML was echoed first

The warning names the file and line where output started, which is the useful half: it points at the leak, not at the cookie. setcookie() returns false rather than throwing, so a script that ignores the return value never sets the cookie and never says so.

This bug travels badly too: if output_buffering is on in php.ini โ€” XAMPP ships it at 4096 bytes โ€” PHP holds the body back, the headers stay open, and the same call succeeds with no warning until the buffer fills. The fix is layout, not configuration. Do the cookie work at the top of the file and print afterwards.

Expiry is a Unix timestamp

The third argument is not a number of seconds and not a date string. It is an absolute Unix timestamp: the moment the cookie should die, counted in seconds since 1 January 1970. That is why examples add to time().

Leave the argument out and you get a session cookie, dropped when the browser closes. That is the right choice more often than people assume.

<?php
$hour = time() + 3600;
$week = time() + (7 * 24 * 60 * 60);

setcookie('one_hour', 'short', $hour);
setcookie('one_week', 'longer', $week);
setcookie('until_close', 'no expiry argument at all');

echo 'time() right now:  ' . time() . "\n";
echo 'one hour from now: ' . $hour . '  ' . date('D, d M Y H:i:s', $hour) . "\n";
echo 'one week from now: ' . $week . '  ' . date('D, d M Y H:i:s', $week) . "\n";

Output:

php cookies output showing the current Unix timestamp beside the one hour and one week expiry values

Passing 3600 on its own is the classic slip. That timestamp lands in January 1970, so the browser discards the cookie the instant it arrives. The expiry is a request, not a guarantee: the user can clear it whenever they like.

Deleting a PHP cookie

There is no unsetcookie(). You delete a cookie by setting it again with an expiry in the past, which tells the browser to throw its copy away.

<?php
setcookie('username', '', time() - 3600);

// the browser has been told to drop it, but this request already had it
var_dump(isset($_COOKIE['username']));

print_r(headers_list());

Output:

php cookies output showing a deletion Set-Cookie header dated 1970 while $_COOKIE still holds the old value

bool(true), still, and that is correct. $_COOKIE holds what arrived at the start of this request, and the browser sent the cookie because it had not been told otherwise yet. If the page must also act as though it has gone, add unset($_COOKIE['username']).

Deletion only works when path and domain match the ones the cookie was created with; delete with defaults a cookie set with path => '/' and you write a second cookie while the original sits untouched.

path, domain and the options array

path and domain decide which requests carry the cookie back. Leave path out and the browser defaults it to the directory of the current script, so a cookie set by /shop/checkout.php never reaches /index.php. Pass '/' for the whole site. Leave domain out and the cookie belongs to the host that set it; name one and its subdomains come too.

The positional form ends at secure and httponly, with no room for samesite. PHP 7.3 added a second signature taking an options array, the only way to set it.

<?php
// positional: name, value, expires, path, domain, secure, httponly
setcookie('theme', 'dark', time() + 86400, '/', '', false, true);

// PHP 7.3 and newer: the same cookie as an options array, plus samesite
setcookie('theme', 'dark', [
    'expires'  => time() + 86400,
    'path'     => '/',
    'secure'   => true,
    'httponly' => true,
    'samesite' => 'Lax',
]);

print_r(headers_list());

Output:

php cookies output showing two Set-Cookie headers, the second one carrying HttpOnly and SameSite Lax

Both headers go out and the second wins. Three flags decide how safely PHP cookies travel. httponly hides the value from document.cookie, so JavaScript injected by an attacker cannot read it. secure stops the browser sending it over plain HTTP, and stops it being stored at all on http://localhost, so expect that second cookie to vanish on XAMPP. samesite decides whether the cookie rides along on requests started by another site: 'Lax' is the sensible default, 'Strict' tighter, and 'None' rejected unless secure is true.

PHP cookies versus sessions

Both survive between requests, but keep the data in opposite places. A cookie lives in the browser, in a file the user owns, so they can open it, edit it and send back whatever they like. A session lives on your server; the browser only gets the id.

<?php
session_start();

$_SESSION['role'] = 'admin';   // kept in a file on the server
setcookie('role', 'admin');    // kept in the browser, in plain text

print_r(headers_list());

echo "\n";
echo 'Session id the browser will hold: ' . session_id() . "\n";
echo 'Role the server will hold:        ' . $_SESSION['role'] . "\n";

Output:

php cookies output showing role=admin in a plain header next to a session id that hides its value

The header says role=admin in plain text, and anyone can edit that word and reload. The session sends a random id and keeps admin out of reach. So cookies carry preferences you would not mind a user rewriting โ€” a theme, a language, a dismissed banner โ€” and anything you make a decision on belongs in a PHP session.

Three mistakes with PHP cookies

Calling setcookie() after the HTML has started. One echo, one blank line before <?php, one include ending in whitespace after ?>, and the headers are gone: "Cannot modify header information โ€” headers already sent". The cookie is silently not set, and the line reported is the output, not the cookie. Move every header call above the first byte of markup.

Expecting $_COOKIE to update in the same request. $_COOKIE is a snapshot of what arrived, not a live view of what the browser holds. Read it back two lines after setting it and you get the old value or none, and the code looks right because it is only wrong on the first request. If the page needs the value now, hold it in a variable too.

Storing a user id or a role in a plain cookie and trusting it. setcookie('user_id', 42) then if ($_COOKIE['role'] === 'admin') on the next page is a login anyone defeats by editing two words. The value is not signed, not encrypted, and not yours once it leaves the server. Put the id in a session instead.

Common questions about PHP cookies

How do I set a cookie in PHP?

setcookie('name', 'value', time() + 3600, '/'); before any output. The third argument is the expiry as a Unix timestamp; the fourth is the path, which you nearly always want to be '/' so the cookie reaches every page.

Why is my cookie not showing in $_COOKIE?

Three causes, in order of likelihood. You are checking on the request that set it, so the browser has not sent it back yet: reload. Or output started before setcookie() ran, so the header never left. Or the path does not cover the page you are on.

How do I delete PHP cookies?

Set the same name again with an expiry in the past, such as time() - 3600, using the same path and domain as the original. Add unset($_COOKIE['name']) if the rest of that request must behave as though it has already gone.

What is the difference between cookies and sessions in PHP?

A cookie stores the data in the browser, where the user can read and change it. A session stores it on the server and puts only an id in the browser. Cookies are for preferences; sessions are for anything the application acts on.

How long do PHP cookies last?

Until the timestamp you pass as the third argument, or until the browser closes if you pass nothing. There is no maximum you can rely on: browsers cap long lifetimes themselves.

What is next

PHP cookies are the first place user-controlled data walks straight back into your code, and not the last: form fields, query strings and uploads arrive the same way. PHP security covers what to do about it: escaping output, validating input, prepared statements and password hashing.

Comments

Loading comments...

Link copied to clipboard