Skip to main content
    Courses/PHP/Sessions & Cookies

    Lesson 10 • Intermediate

    Sessions & Cookies 🍪

    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.

    What You'll Learn in This Lesson

    • Start a session with session_start() and explain how PHPSESSID works
    • Read and write data with the $_SESSION superglobal
    • Build a login / logout pattern with isset() and session_destroy()
    • Set cookies with expiry, path, secure, httponly and samesite
    • Read cookies from $_COOKIE and delete them safely
    • Defend against session fixation with session_regenerate_id()

    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.

    Start a session and store data
    <?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';
    
    echo "Saved! Your session ID is: " . session_id() . "\n";
    echo "Stored username: " . $_SESSION['username'] . "\n";
    ?>
    Output
    Saved! Your session ID is: 8f3c1a9b7e2d4f60a1c5
    Stored username: alice
    This is real code — run it for free atonecompiler.com/phpor in your own editor.

    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.

    Read with isset(), remove with unset()
    <?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();
    
    // 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).
    ?>
    Output
    Welcome back, alice!
    Your role is: admin
    role removed
    This is real code — run it for free atonecompiler.com/phpor in your own editor.

    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.

    login(), isLoggedIn(), logout()
    <?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";
    ?>
    Output
    Logged in as alice
    Logged out
    This is real code — run it for free atonecompiler.com/phpor in your own editor.

    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.

    Set cookies with secure options
    <?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";
    ?>
    Output
    Two cookies were sent in the response headers.
    They will appear in $_COOKIE on the NEXT request.
    This is real code — run it for free atonecompiler.com/phpor in your own editor.

    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.

    Read $_COOKIE, delete with a past expiry
    <?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
    
    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";
    ?>
    Output
    Your theme is: dark
    Language: en
    The theme cookie has been told to expire.
    This is real code — run it for free atonecompiler.com/phpor in your own editor.

    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.

    🎯 Your turn: start a session and read it
    <?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
    ?>
    Output
    Logged in. User #7
    Fill the first blank with the function that starts a session and the second with the function that tests whether a key is set. Run it on a server — the output should be one line.

    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.

    🎯 Your turn: set and read a cookie
    <?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
    ?>
    Output
    Theme: light
    Fill the function name, then put true in both flag blanks and "light" as the fallback. On the very first load (before the cookie returns) the output is Theme: light.

    Common Errors (and the fix)

    • "Warning: session_start(): Cannot start session — headers already sent" — something printed before session_start() (or setcookie()). The usual culprit is a blank line or space before the opening <?php tag, or an echo above it. Move both calls to the very top and strip any whitespace before <?php.
    • Login "works" but the session can be hijacked — you didn't regenerate the ID. After every successful login call session_regenerate_id(true) so the session ID changes; otherwise an attacker who fixed the ID earlier rides your logged-in session (session fixation).
    • A login cookie can be read by JavaScript or sent over plain HTTP — you left off the security flags. Set 'httponly' => true (blocks JavaScript / XSS theft) and 'secure' => true (HTTPS only), and add 'samesite' => 'Strict' to blunt CSRF.
    • "Undefined array key" when reading $_SESSION or $_COOKIE — you read a key that isn't there. Guard with isset($_SESSION['key']) or use a default: $_COOKIE['x'] ?? 'fallback'.
    • $_COOKIE is empty right after setcookie() — that's expected, not a bug. The cookie only arrives on the next request. Reload the page (or move to the next one) and it will be there.

    Pro Tips

    • 💡 Secrets go in the session, not the cookie. The browser only ever needs the meaningless PHPSESSID — keep usernames, roles, and tokens server-side in $_SESSION.
    • 💡 Regenerate on every privilege change, not just login — for example after switching to an admin area. It's cheap insurance against fixation.
    • 💡 Prefer the array form of setcookie() (PHP 7.3+). Named options like 'samesite' are clearer and harder to get wrong than a long list of positional arguments.

    📋 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)

    Frequently Asked Questions

    Q: 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.

    Q: 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.

    Q: 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.

    Q: 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.

    Q: 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().

    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.

    🎯 Mini-Challenge: count and remember page views
    <?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
    ?>
    Start a session, default $_SESSION['views'] to 0, add 1, and echo the count. Refresh to see it grow: 1, then 2, then 3.

    🎉 Lesson Complete!

    • ✅ HTTP is stateless; a session remembers a user via the PHPSESSID cookie and server-side storage
    • session_start() must run before any output; read/write data through $_SESSION
    • ✅ Guard reads with isset(); remove with unset() or session_destroy()
    • Cookies live in the browser — set them with setcookie() and read them from $_COOKIE on the next request
    • ✅ Lock cookies down with secure, httponly, and samesite; regenerate the ID at login to stop fixation
    • Next lesson: Object-Oriented PHP — organise your code into reusable classes and objects

    Sign up for free to track which lessons you've completed and get learning reminders.

    Previous

    Cookie & Privacy Settings

    We use cookies to improve your experience, analyze traffic, and show personalized ads. You can manage your preferences below.

    By clicking "Accept All", you consent to our use of cookies for analytics and personalized advertising. You can customize your preferences or reject non-essential cookies.

    Privacy PolicyTerms of Service