Sessions & Cookies

Reviewed & published by Brayan K

By the end of this lesson you'll be able to remember a user across page loads — keeping login state in a secure server-side session and storing harmless preferences in a browser cookie, the right way.

Part of the free PHP course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.

What You'll Learn in This Lesson

1️⃣ Sessions: Remembering a User

The web is stateless — by default the server forgets everything the moment a page finishes loading, so the next click looks like a brand-new stranger. A session fixes that. Calling session_start() gives the visitor a unique ID, stores that ID in a cookie called PHPSESSID, and opens a private file on the server to hold their data. You read and write that data through the $_SESSION superglobal — an array PHP makes available in every file. Because the data lives on the server, the user can't see or tamper with it.

<?php
// HTTP is "stateless" — the server forgets you the instant a page finishes
// loading. A SESSION is how PHP remembers you across page loads.
//
// session_start() does the remembering. On the FIRST visit it:
//   1. generates a unique session ID (a long random string)
//   2. sends that ID to the browser in a cookie named PHPSESSID
//   3. creates a file on the server to hold YOUR data
// On every later visit the browser sends PHPSESSID back, and PHP reopens
// the matching file — so your data is waiting for you.

session_start();   // ALWAYS the first thing, before any HTML or echo

// $_SESSION is a "superglobal" — an array PHP fills in for you, readable
// from any file once the session is started. Write to it like any array:
$_SESSION['username'] = 'alice';   // stored on the SERVER, not the browser
$_SESSION['role']     = 'admin';

// The ID is random — that is the whole point of it — so never print the value.
// Its LENGTH is not fixed either: session.sid_length in php.ini decides it, and
// 26 and 32 are both common defaults. What matters is that it is long enough to
// be unguessable, so check that instead of claiming a number.
echo "Saved! Session ID is at least 26 characters: "
   . (strlen(session_id()) >= 26 ? "yes" : "no") . "\n";
echo "Stored username: " . $_SESSION['username'] . "\n";
?>

One rule dominates everything here: session_start() must run before any output — no HTML, no echo, not even a stray blank line before <?php. It sends an HTTP header, and headers must come first. Put it at the very top of the file, every time.

2️⃣ Reading & Removing Session Data

On a later page you call session_start() again — not to recreate the data, but to reconnect to it using the ID the browser sent back. Always guard a read with isset(), which returns true only when a key exists; reading a missing key throws a warning. To remove data you have two tools: unset($_SESSION['key']) deletes a single value, and you'll see session_destroy() below for clearing everything.

<?php
// On a LATER page (e.g. dashboard.php) you start the session again to
// reconnect to the same data — you do NOT set the values a second time.
session_start();

// Running this on its own? Then no earlier page put anything in the session,
// so stand in for that page here and the isset() branch below has something
// to find. On a real second page you would delete these two lines.
$_SESSION['username'] = $_SESSION['username'] ?? 'alice';
$_SESSION['role']     = $_SESSION['role']     ?? 'admin';

// Always check a key EXISTS before reading it. isset() returns true only
// if the key is present and not null — this avoids "Undefined index" warnings.
if (isset($_SESSION['username'])) {
    echo "Welcome back, " . $_SESSION['username'] . "!\n";   // Welcome back, alice!
    echo "Your role is: " . $_SESSION['role'] . "\n";        // Your role is: admin
} else {
    echo "No one is logged in.\n";
}

// Removing data:
unset($_SESSION['role']);   // delete ONE key — 'username' still remains
echo isset($_SESSION['role']) ? "role still here\n" : "role removed\n";

// session_destroy() throws away the WHOLE session (used at logout, below).
?>

3️⃣ The Login / Logout Pattern

Login state is just session data. When a login succeeds you store something that identifies the user (a user_id); protected pages then check isset($_SESSION['user_id']) to decide who gets in; logout clears it. The one line beginners skip is session_regenerate_id(true) at login — it swaps in a fresh session ID so an attacker can't hijack the session (more on this in Security below). Logout empties $_SESSION and calls session_destroy() to delete the server file.

<?php
// The classic login-state pattern: store a user id when login succeeds,
// check for it on protected pages, clear it on logout.

session_start();

function login(string $username): void {
    // SECURITY: regenerate the ID the moment privileges change. This swaps
    // the old session ID for a fresh one so an attacker who planted an ID
    // earlier (session fixation) cannot ride your now-logged-in session.
    session_regenerate_id(true);          // true = delete the old session file
    $_SESSION['user_id']  = 42;
    $_SESSION['username'] = $username;
}

function isLoggedIn(): bool {
    return isset($_SESSION['user_id']);   // true once login() has run
}

function logout(): void {
    $_SESSION = [];        // 1) empty the data array
    session_destroy();     // 2) delete the session file on the server
}

login('alice');
echo isLoggedIn() ? "Logged in as {$_SESSION['username']}\n" : "Guest\n";

logout();
session_start();           // start a fresh, empty session to check
echo isLoggedIn() ? "Still logged in\n" : "Logged out\n";
?>

4️⃣ Cookies: Storing Data in the Browser

A cookie is a small key-value pair (up to about 4KB) that PHP asks the browser to store and send back on every future request. Use cookies for things that aren't secret — a theme, a chosen language, a "remember me" flag. You create one with setcookie(), and the modern form takes an options array so the security flags are clearly named. Like session_start(), it sends a header, so it must run before any output.

<?php
// A COOKIE is a small piece of data stored in the BROWSER (max ~4KB).
// The browser sends it back on every request to the same site. Cookies are
// perfect for non-secret preferences: theme, language, "remember me".
//
// setcookie() sends a Set-Cookie HEADER, so — like session_start() — it must
// run BEFORE any output. Modern signature uses an options array:

setcookie('theme', 'dark', [
    'expires'  => time() + 60 * 60 * 24 * 30,  // now + 30 days (a UNIX timestamp)
    'path'     => '/',          // send the cookie for the WHOLE site, not just one folder
    'secure'   => true,         // only sent over HTTPS — never plain http
    'httponly' => true,         // JavaScript canNOT read it (blocks XSS theft)
    'samesite' => 'Strict',     // don't send on cross-site requests (blocks CSRF)
]);

setcookie('lang', 'en', [
    'expires'  => time() + 60 * 60 * 24 * 365,  // 1 year
    'path'     => '/',
]);

echo "Two cookies were sent in the response headers.\n";
echo "They will appear in \$_COOKIE on the NEXT request.\n";
?>

Each option earns its place: expires is a UNIX timestamp for when the cookie dies (omit it and the cookie vanishes when the browser closes); path set to '/' makes the cookie apply site-wide; secure restricts it to HTTPS; httponly hides it from JavaScript; and samesite blocks it on cross-site requests. The last three are your front-line defence and you'll meet them again in Security.

5️⃣ Reading & Deleting Cookies

Here's the catch that trips everyone the first time: a cookie you just set is not in $_COOKIE on the same request. setcookie() only tells the browser to store it; the browser sends it back starting from the next request, which is when PHP fills the $_COOKIE superglobal. To read it, guard with isset() or the null-coalescing ?? operator for a fallback. To delete a cookie, re-set it with an expiry in the past.

<?php
// On the NEXT request the browser sends the cookies back, and PHP fills the
// $_COOKIE superglobal for you. (No session_start() needed for cookies.)
//
// Imagine the browser sent: theme=dark; lang=en — which, without a browser,
// means putting it there yourself:
$_COOKIE = ['theme' => 'dark', 'lang' => 'en'];

if (isset($_COOKIE['theme'])) {
    echo "Your theme is: " . $_COOKIE['theme'] . "\n";   // Your theme is: dark
} else {
    echo "No theme set — using the default.\n";
}

// A null-coalescing default (??) is the tidy way to read with a fallback:
$lang = $_COOKIE['lang'] ?? 'en';   // use 'en' if the cookie is missing
echo "Language: " . $lang . "\n";  // Language: en

// DELETE a cookie by re-setting it with an expiry time in the PAST:
setcookie('theme', '', ['expires' => time() - 3600, 'path' => '/']);
echo "The theme cookie has been told to expire.\n";
?>

6️⃣ Security: Fixation, httponly & secure

State is where most beginner security holes live, so internalise three habits. Session fixation: an attacker gets a victim to use a session ID the attacker already knows, then waits for the victim to log in and rides the now-authenticated session. Defeat it by calling session_regenerate_id(true) the instant a login succeeds — the ID changes, so the attacker's copy is worthless. Cookie theft: a cross-site scripting (XSS) flaw lets injected JavaScript read document.cookie — unless the cookie is httponly: true, which hides it from JavaScript entirely. Eavesdropping & CSRF: secure: true keeps the cookie off plain HTTP, and samesite: 'Strict' (or 'Lax') stops it riding along on requests from other sites. For any cookie tied to a login, set all three — and never store passwords or raw tokens in a cookie; keep secrets in the session and store only a hashed token if you must persist one.

7️⃣ Your Turn: Check Login State

Now you try. The script below is almost complete — fill in each ___ using the 👉 hint, then run it and check it against the Output panel.

<?php
// 🎯 YOUR TURN — finish the login check. Fill each blank marked ___ , then run.

// 1) Start the session so $_SESSION works
___;                           // 👉 the function that starts/resumes a session

$_SESSION['user_id'] = 7;      // pretend a login already happened

// 2) Read the value safely — fill in the function that tests a key exists
if (___($_SESSION['user_id'])) {   // 👉 returns true only if the key is set
    echo "Logged in. User #" . $_SESSION['user_id'] . "\n";
} else {
    echo "Not logged in\n";
}

// ✅ Expected output:
//    Logged in. User #7
?>

One more. This time you set a secure cookie and read it back with a sensible default. Fill in the function name and the two security flags.

<?php
// 🎯 YOUR TURN — set ONE secure cookie, then read it back with a default.

// 1) Store the user's chosen theme for 7 days. Fill in the function name
//    and the two security flags that block JS access and force HTTPS.
___('theme', 'dark', [          // 👉 the function that sends a Set-Cookie header
    'expires'  => time() + 60 * 60 * 24 * 7,
    'path'     => '/',
    'secure'   => ___,          // 👉 true — HTTPS only
    'httponly' => ___,          // 👉 true — hide it from JavaScript
]);

// 2) On the next request, read it with 'light' as the fallback.
$theme = $_COOKIE['theme'] ?? ___;   // 👉 the default value in quotes: "light"
echo "Theme: " . $theme . "\n";

// ✅ Expected output (first run, before the cookie comes back): Theme: light
//    (after the browser returns the cookie):                    Theme: dark
?>

Common Errors (and the fix)

Pro Tips

📋 Quick Reference — Sessions & Cookies

SyntaxExampleWhat It Does
session_start()session_start();Start/resume the session (before any output)
$_SESSION['k']$_SESSION['id'] = 7;Read / write server-side session data
unset()unset($_SESSION['k']);Remove one session variable
session_destroy()session_destroy();Destroy the whole session (logout)
session_regenerate_id()session_regenerate_id(true);New ID — prevents session fixation
setcookie()setcookie('t','dark',[...]);Create / update / delete a cookie
$_COOKIE['name']$_COOKIE['t'] ?? 'light'Read a cookie (next request)

Mini-Challenge: A Page-View Counter

No code is filled in this time — just a brief and an outline. Write it yourself, serve it with php -S localhost:8000, then refresh the page a few times and watch the count climb. This is the same store-read-update loop behind every shopping cart and login on the web.

<?php
// 🎯 MINI-CHALLENGE: A page-view counter that survives reloads.
// No code is filled in — work from the steps below, then run it and refresh.
//
// 1. Start the session.
// 2. If $_SESSION['views'] is NOT set, set it to 0.
//    (Tip: $_SESSION['views'] = $_SESSION['views'] ?? 0;)
// 3. Add 1 to $_SESSION['views'].
// 4. echo: "You have viewed this page X time(s)."
// 5. SECURITY: on a real login you'd also call session_regenerate_id(true)
//    — add a comment saying WHY (it prevents session fixation).
//
// ✅ Expected output:
//    1st load:  You have viewed this page 1 time(s).
//    2nd load:  You have viewed this page 2 time(s).
//    3rd load:  You have viewed this page 3 time(s).

// your code here
?>

🎉 Lesson Complete!

Practice quiz

Where is session data stored?

  • Entirely in the browser cookie
  • In the URL query string
  • On the server; only the session ID is in the browser
  • In a hidden form field

Answer: On the server; only the session ID is in the browser. Session data lives in a file on the server; the browser only holds the meaningless PHPSESSID cookie.

What must be true about calling session_start() and setcookie()?

  • They must run before any output (HTML or echo)
  • They must run after the HTML body
  • They can be called anywhere
  • They only work inside functions

Answer: They must run before any output (HTML or echo). Both send HTTP headers, which must come before any page content — otherwise 'headers already sent'.

Why call session_regenerate_id(true) right after a successful login?

  • To log the user out
  • To clear all cookies
  • To speed up the session
  • To prevent session fixation by swapping in a fresh ID

Answer: To prevent session fixation by swapping in a fresh ID. Regenerating the ID at login means an attacker's pre-planted ID no longer points at the logged-in session.

When does a cookie you just set with setcookie() appear in $_COOKIE?

  • Immediately on the same request
  • On the next request, not the current one
  • Never
  • Only after session_destroy()

Answer: On the next request, not the current one. setcookie() only sends a header; the browser returns the cookie starting from the next request.

Which cookie flag hides the cookie from JavaScript to block XSS theft?

  • httponly
  • secure
  • samesite
  • path

Answer: httponly. httponly: true hides the cookie from document.cookie, so injected JavaScript cannot read it.

Which cookie flag ensures the cookie is only sent over HTTPS?

  • httponly
  • samesite
  • secure
  • expires

Answer: secure. secure: true keeps the cookie off plain HTTP, so it is never exposed in transit.

How do you delete a cookie?

  • Call unset($_COOKIE['name'])
  • Re-set it with an expiry time in the past
  • Call session_destroy()
  • Set its value to null

Answer: Re-set it with an expiry time in the past. Re-setting the cookie with expires in the past (e.g. time() - 3600) tells the browser to discard it.

What is the right pair of steps to log a user out?

  • Only unset($_SESSION['user_id']);
  • Only setcookie('PHPSESSID', '');
  • Call session_start() again
  • $_SESSION = []; then session_destroy();

Answer: $_SESSION = []; then session_destroy();. Empty the data array, then session_destroy() deletes the session file on the server.

How should you safely read a possibly-missing session key?

  • Read it directly and ignore warnings
  • Guard with isset($_SESSION['key'])
  • Use session_destroy() first
  • Cast it to a string

Answer: Guard with isset($_SESSION['key']). isset() returns true only if the key exists and isn't null, avoiding 'Undefined array key' warnings.

What is the key difference between a session and a cookie?

  • Cookies are always encrypted; sessions are not
  • Sessions live in the browser; cookies live on the server
  • A session stores data on the server; a cookie stores it in the browser
  • They are identical

Answer: A session stores data on the server; a cookie stores it in the browser. Use sessions for anything that matters (login state); use cookies for harmless preferences like theme.

Continue this course

Frequently asked questions

What is the difference between a session and a cookie?

A cookie stores data in the browser; a session stores data on the server. With a session, the only thing the browser holds is a meaningless ID (the PHPSESSID cookie) — the real data (your username, role, cart) lives in a file on the server where users cannot see or change it. Cookies are limited to about 4KB and can be read and altered by the user, so use cookies for harmless preferences (theme, language) and sessions for anything that matters, like login state.

Why do I get "headers already sent" or "Cannot modify header information"?

Both session_start() and setcookie() work by sending HTTP headers, and headers must come before any page content. If even a single space, blank line, or echo runs before them, PHP has already started sending the body and the headers are locked. The fix is to call session_start() and setcookie() at the very top of the file, before any HTML or output — and make sure there is no whitespace before the opening <?php tag.

What does session_regenerate_id() do and when should I call it?

It replaces the current session ID with a brand-new one while keeping the data. Call it immediately after a privilege change — most importantly right after a successful login. This defends against session fixation, where an attacker tricks a victim into using a session ID the attacker already knows; regenerating the ID at login means the attacker's ID no longer points at the logged-in session. Pass true (session_regenerate_id(true)) to also delete the old session file.

What do the secure, httponly, and samesite cookie flags do?

secure tells the browser to send the cookie only over HTTPS, so it is never exposed on plain http. httponly hides the cookie from JavaScript (document.cookie), which stops a cross-site scripting (XSS) attack from stealing it. samesite controls whether the cookie is sent on requests coming from other sites — Strict or Lax helps block cross-site request forgery (CSRF). For any cookie tied to a login, set all three.

Why is my $_COOKIE empty right after I call setcookie()?

Cookies are not available on the same request that sets them. setcookie() only adds a header telling the browser to store the cookie; the browser then sends it back starting from the next request, which is when PHP populates $_COOKIE. So you will see the value on a reload or the next page, not on the line directly after setcookie().