Skip to main content

🔐 Lesson 15: Sessions & Cookies

HTTP is stateless — every request starts from scratch with no memory of previous requests. But web applications need state: logins, shopping carts, user preferences, and more. Sessions and cookies are the two mechanisms PHP provides to bridge this gap and make the web feel stateful.

🎯 Learning Objectives

By the end of this lesson, you will be able to:

  • Explain the stateless nature of HTTP and why sessions/cookies exist
  • Start, use, and destroy PHP sessions with session_start() and $_SESSION
  • Set, read, and delete cookies with setcookie() and $_COOKIE
  • Build a complete login/logout system using sessions
  • Implement "remember me" functionality with cookies
  • Apply session and cookie security best practices

Estimated Time: 45 minutes

Prerequisites: Lesson 14 (Superglobals), Lessons 11–12 (forms, validation)

📑 In This Lesson

The Stateless Problem

Every HTTP request is independent. When your browser asks for /dashboard.php, the server has no idea that you just logged in via /login.php two seconds ago. Each request is a fresh start — no memory, no context, no identity.

This is a problem for almost every real web application:

  • Login systems — "Who is this user? Did they already authenticate?"
  • Shopping carts — "What did this person add before visiting the checkout page?"
  • User preferences — "Should I show dark mode? What language do they prefer?"
  • Multi-step forms — "What did they enter on step 1 while they're on step 3?"

The solution: cookies (small data stored in the browser) and sessions (data stored on the server, linked to the browser via a cookie).

sequenceDiagram participant B as Browser participant S as Server B->>S: POST /login.php (username, password) S->>S: Validate credentials S->>B: Set-Cookie: PHPSESSID=abc123 Note over B: Browser stores cookie B->>S: GET /dashboard.php (Cookie: PHPSESSID=abc123) S->>S: Look up session abc123 S->>B: Welcome back, Alice! B->>S: GET /profile.php (Cookie: PHPSESSID=abc123) S->>S: Same session → same user S->>B: Alice's profile page

Cookies — Client-Side Storage

A cookie is a small piece of data (up to ~4 KB) that the server tells the browser to store. The browser then sends it back with every subsequent request to the same domain. Cookies are stored on the user's machine, which means the user can see, modify, or delete them.

Setting Cookies with setcookie()

<?php
// setcookie(name, value, expires, path, domain, secure, httponly)

// Basic cookie — expires when browser closes (session cookie)
setcookie("username", "alice");

// Cookie that expires in 30 days
setcookie("theme", "dark", time() + (30 * 24 * 60 * 60));

// Cookie with specific path (only sent for /admin/ pages)
setcookie("admin_token", "xyz", time() + 3600, "/admin/");

// Secure cookie (HTTPS only + HttpOnly flag)
setcookie("session_id", "abc123", [
    "expires"  => time() + 3600,
    "path"     => "/",
    "domain"   => "",           // Current domain
    "secure"   => true,         // Only send over HTTPS
    "httponly"  => true,         // JavaScript can't access it
    "samesite" => "Strict",     // CSRF protection
]);

⚠️ setcookie() Must Come Before Any Output

Cookies are sent as HTTP headers, and headers must be sent before any HTML, whitespace, or echo output. If you call setcookie() after output has started, you'll get a "headers already sent" warning and the cookie won't be set. Place all setcookie() calls at the very top of your script.

Reading Cookies with $_COOKIE

<?php
// Cookies set on the previous request are available in $_COOKIE
// NOTE: Cookies you JUST set with setcookie() are NOT in $_COOKIE
//       on the same request — they arrive on the NEXT request

// Read a cookie (with a default)
$theme = $_COOKIE["theme"] ?? "light";
$username = $_COOKIE["username"] ?? "Guest";

echo "Theme: $theme\n";      // "dark" (if set previously)
echo "Hello, $username!\n";  // "Hello, alice!"

// Check if a cookie exists
if (isset($_COOKIE["username"])) {
    echo "Welcome back, " . htmlspecialchars($_COOKIE["username"]);
} else {
    echo "Hello, new visitor!";
}

Deleting Cookies

<?php
// To delete a cookie, set it with an expiration in the past
setcookie("username", "", time() - 3600);

// For cookies set with specific path/domain, you must match them
setcookie("admin_token", "", time() - 3600, "/admin/");

// Using the array syntax
setcookie("session_id", "", [
    "expires"  => time() - 3600,
    "path"     => "/",
    "secure"   => true,
    "httponly"  => true,
    "samesite" => "Strict",
]);

// The cookie won't disappear from $_COOKIE until the next request
// To stop using it immediately:
unset($_COOKIE["username"]);

Cookie Limitations

Limitation Details
Size ~4 KB per cookie, ~50 cookies per domain
Security User can read, modify, or delete any cookie
Privacy Sent with every HTTP request (adds bandwidth)
Timing Not available on the same request they're set
Data types Strings only (no arrays or objects directly)

✅ When to Use Cookies

Cookies are best for non-sensitive preferences that should persist even after the browser closes: theme choice, language preference, "remember me" tokens, accepted cookie banners. For sensitive data like login state, use sessions instead.

Sessions — Server-Side Storage

A session stores data on the server, linked to a specific user via a session ID cookie. The user only has the key (the session ID) — the actual data stays on the server where they can't tamper with it.

Starting a Session

<?php
// session_start() MUST be called before:
// 1. Any use of $_SESSION
// 2. Any output (HTML, echo, whitespace before <?php)

session_start();

// Now $_SESSION is available and linked to this user's session
// If a session already exists (from a previous request), it resumes
// If not, PHP creates a new one and sends a PHPSESSID cookie

⚠️ session_start() Must Come First

Just like setcookie(), session_start() sends a header (the session ID cookie). It must be called before any output. The safest approach: make session_start() the very first line of your script, or place it in an included config file.

Storing Data in Sessions

<?php
session_start();

// Store any type of data — strings, numbers, arrays, objects
$_SESSION["user_id"]    = 42;
$_SESSION["username"]   = "alice";
$_SESSION["role"]       = "admin";
$_SESSION["login_time"] = time();
$_SESSION["cart"]       = [
    ["product" => "Widget", "qty" => 2, "price" => 12.99],
    ["product" => "Gadget", "qty" => 1, "price" => 49.99],
];

// Data persists across page loads!
// Navigate to another page, and these values are still there

Reading Session Data

<?php
session_start();

// Check if the user is logged in
if (isset($_SESSION["user_id"])) {
    $username = htmlspecialchars($_SESSION["username"]);
    echo "Welcome back, $username!";
    echo "You logged in at " . date("g:i A", $_SESSION["login_time"]);

    // Access cart data
    $cartCount = count($_SESSION["cart"] ?? []);
    echo "You have $cartCount items in your cart.";
} else {
    echo "You are not logged in.";
    echo '<a href="login.php">Log in</a>';
}

Modifying and Removing Session Data

<?php
session_start();

// Update a value
$_SESSION["page_views"] = ($_SESSION["page_views"] ?? 0) + 1;

// Remove a specific key
unset($_SESSION["cart"]);

// Remove ALL session data (but keep the session alive)
$_SESSION = [];

// Completely destroy the session (logout)
session_start();
$_SESSION = [];

// Delete the session cookie
if (ini_get("session.use_cookies")) {
    $params = session_get_cookie_params();
    setcookie(
        session_name(),      // Usually "PHPSESSID"
        "",
        time() - 42000,
        $params["path"],
        $params["domain"],
        $params["secure"],
        $params["httponly"]
    );
}

// Destroy the session file on the server
session_destroy();

Session vs. Cookie Comparison

Feature Cookie Session
Storage location Browser (client) Server (file by default)
Size limit ~4 KB Server memory/disk
Data types Strings only Any PHP type
User can read? Yes No (only the session ID)
User can modify? Yes No
Lifetime Set via expiration Until timeout or destroy
Survives browser close? Yes (if expires is set) No (default)
Use for sensitive data? ❌ Never ✅ Yes

How Sessions Work Under the Hood

Understanding the mechanics helps you debug session problems and configure them correctly.

flowchart TD A["session_start()"] --> B{"Session ID cookie exists?"} B -- "No" --> C["Generate new session ID"] C --> D["Create session file on server"] D --> E["Send Set-Cookie: PHPSESSID=xyz"] B -- "Yes" --> F["Read session ID from cookie"] F --> G["Load session file from server"] G --> H["Populate $_SESSION"] E --> H H --> I["Your script runs"] I --> J["Script ends"] J --> K["Write $_SESSION back to file"] style A fill:#dbeafe,stroke:#3b82f6 style I fill:#dcfce7,stroke:#22c55e style K fill:#fef3c7,stroke:#f59e0b

Step by Step

  1. session_start() checks if the browser sent a PHPSESSID cookie
  2. If no cookie exists: PHP generates a random session ID (e.g., abc123def456), creates a file on the server (e.g., /tmp/sess_abc123def456), and sends a Set-Cookie header
  3. If a cookie exists: PHP reads the session ID, finds the corresponding file, and loads its data into $_SESSION
  4. Your script reads and writes $_SESSION as a normal array
  5. When the script ends, PHP serializes $_SESSION and writes it back to the file

Where Session Files Live

<?php
// Find the session save path
echo session_save_path();
// Usually: "/tmp" or "/var/lib/php/sessions"

// What's in a session file?
// File: /tmp/sess_abc123def456
// Contents: user_id|i:42;username|s:5:"alice";role|s:5:"admin";

// This is PHP's serialization format — you don't need to parse it,
// but it's useful to know what's happening behind the scenes

Session Configuration

<?php
// Configure session settings BEFORE session_start()
// These can also be set in php.ini

// Session lifetime (how long the cookie lasts in the browser)
ini_set("session.cookie_lifetime", 0);  // 0 = until browser closes

// Session garbage collection (how long files are kept on the server)
ini_set("session.gc_maxlifetime", 1440); // 1440 seconds = 24 minutes

// Cookie settings for security
ini_set("session.cookie_httponly", 1);   // Block JavaScript access
ini_set("session.cookie_secure", 1);     // HTTPS only
ini_set("session.cookie_samesite", "Strict"); // CSRF protection

// Use cookies only (not URL-based session IDs)
ini_set("session.use_only_cookies", 1);
ini_set("session.use_trans_sid", 0);     // Don't append to URLs

session_start();

✅ The Key Insight

Sessions use a cookie too — but it's a tiny cookie containing just the session ID (a random string). The actual data stays on the server. This means the user can't see or tamper with their session data — they only have a "ticket number" that the server uses to look up their information.

Building a Login/Logout System

The most common use of sessions: authentication. Let's build a complete login/logout system from scratch.

Project Structure

auth-demo/
├── config.php          ← Session config + helper functions
├── login.php           ← Login form + processing
├── logout.php          ← Session destruction
├── dashboard.php       ← Protected page
└── register.php        ← (Optional) User registration

config.php — Session Setup & Helpers

<?php
// config.php — include this at the top of every page

// Configure session before starting
ini_set("session.cookie_httponly", 1);
ini_set("session.use_only_cookies", 1);
session_start();

// Fake user database (in a real app, use MySQL + password_hash)
define("USERS", [
    "alice" => [
        "id"       => 1,
        "password" => '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', // "password"
        "name"     => "Alice Johnson",
        "role"     => "admin",
    ],
    "bob" => [
        "id"       => 2,
        "password" => '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', // "password"
        "name"     => "Bob Smith",
        "role"     => "user",
    ],
]);

/**
 * Check if the current user is logged in.
 */
function is_logged_in(): bool {
    return isset($_SESSION["user_id"]);
}

/**
 * Require authentication — redirect to login if not logged in.
 */
function require_auth(): void {
    if (!is_logged_in()) {
        $_SESSION["flash"] = "Please log in to access that page.";
        header("Location: login.php");
        exit;
    }
}

/**
 * Get a flash message (one-time display) and remove it.
 */
function get_flash(): ?string {
    $message = $_SESSION["flash"] ?? null;
    unset($_SESSION["flash"]);
    return $message;
}

/**
 * Escape HTML output.
 */
function e(string $text): string {
    return htmlspecialchars($text, ENT_QUOTES, "UTF-8");
}

login.php — Login Form & Processing

<?php
require "config.php";

// If already logged in, redirect to dashboard
if (is_logged_in()) {
    header("Location: dashboard.php");
    exit;
}

$error = "";

if ($_SERVER["REQUEST_METHOD"] === "POST") {
    $username = trim($_POST["username"] ?? "");
    $password = $_POST["password"] ?? "";

    // Look up the user
    if (isset(USERS[$username])) {
        $user = USERS[$username];

        // Verify password against stored hash
        if (password_verify($password, $user["password"])) {
            // Regenerate session ID to prevent session fixation
            session_regenerate_id(true);

            // Store user data in session
            $_SESSION["user_id"]    = $user["id"];
            $_SESSION["username"]   = $username;
            $_SESSION["name"]       = $user["name"];
            $_SESSION["role"]       = $user["role"];
            $_SESSION["login_time"] = time();

            // Redirect to dashboard
            $_SESSION["flash"] = "Welcome back, " . $user["name"] . "!";
            header("Location: dashboard.php");
            exit;
        }
    }

    // Generic error (don't reveal whether username or password was wrong)
    $error = "Invalid username or password.";
}

$flash = get_flash();
?>
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Login</title>
</head>
<body>
    <h1>Login</h1>

    <?php if ($flash): ?>
        <p style="color: blue;"><?= e($flash) ?></p>
    <?php endif; ?>

    <?php if ($error): ?>
        <p style="color: red;"><?= e($error) ?></p>
    <?php endif; ?>

    <form method="post" action="">
        <label>
            Username:
            <input type="text" name="username" required
                   value="<?= e($_POST["username"] ?? "") ?>">
        </label><br><br>

        <label>
            Password:
            <input type="password" name="password" required>
        </label><br><br>

        <button type="submit">Log In</button>
    </form>
</body>
</html>

dashboard.php — Protected Page

<?php
require "config.php";
require_auth(); // Redirects to login if not authenticated

$flash = get_flash();
?>
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Dashboard</title>
</head>
<body>
    <h1>Dashboard</h1>

    <?php if ($flash): ?>
        <p style="color: green;"><?= e($flash) ?></p>
    <?php endif; ?>

    <p>Hello, <strong><?= e($_SESSION["name"]) ?></strong>!</p>
    <p>Role: <?= e($_SESSION["role"]) ?></p>
    <p>Logged in at: <?= date("g:i A", $_SESSION["login_time"]) ?></p>
    <p>Session ID: <?= e(session_id()) ?></p>

    <p><a href="logout.php">Log Out</a></p>
</body>
</html>

logout.php — Destroy Session

<?php
require "config.php";

// 1. Clear all session data
$_SESSION = [];

// 2. Delete the session cookie
if (ini_get("session.use_cookies")) {
    $p = session_get_cookie_params();
    setcookie(session_name(), "", time() - 42000,
        $p["path"], $p["domain"], $p["secure"], $p["httponly"]);
}

// 3. Destroy the session file
session_destroy();

// 4. Redirect to login
header("Location: login.php");
exit;

✅ This Is a Real Authentication Pattern

This is essentially how every PHP application handles authentication. Frameworks like Laravel add more layers (middleware, CSRF tokens, rate limiting), but the core flow is identical: verify credentials → regenerate session ID → store user data in $_SESSION → redirect.

Remember Me with Cookies

Sessions end when the browser closes (by default). A "remember me" feature keeps users logged in across browser restarts by storing a secure token in a long-lived cookie.

The Concept

flowchart TD A["User logs in with 'Remember Me' checked"] --> B["Generate random token"] B --> C["Store token + user_id in database"] B --> D["Store token in long-lived cookie"] E["User returns later (session expired)"] --> F{"Remember me cookie exists?"} F -- "Yes" --> G["Look up token in database"] G --> H{"Token valid?"} H -- "Yes" --> I["Start new session for user"] H -- "No" --> J["Show login form"] F -- "No" --> J style A fill:#dbeafe,stroke:#3b82f6 style I fill:#dcfce7,stroke:#22c55e style J fill:#fee2e2,stroke:#ef4444

Implementation (Simplified)

<?php
// === IN login.php (after successful authentication) ===

if (!empty($_POST["remember_me"])) {
    // Generate a secure random token
    $token = bin2hex(random_bytes(32)); // 64 hex characters

    // In a real app: store this in a database table
    // INSERT INTO remember_tokens (user_id, token, expires_at)
    // VALUES (:user_id, :token_hash, :expires)
    // IMPORTANT: Store the HASH of the token, not the token itself
    $tokenHash = hash("sha256", $token);

    // For this demo, store in a JSON file
    $tokens = json_decode(file_get_contents("data/tokens.json") ?: "[]", true);
    $tokens[] = [
        "user_id"    => $_SESSION["user_id"],
        "token_hash" => $tokenHash,
        "expires"    => time() + (30 * 24 * 60 * 60), // 30 days
    ];
    file_put_contents("data/tokens.json", json_encode($tokens));

    // Send the token (NOT the hash) to the browser as a cookie
    setcookie("remember_token", $token, [
        "expires"  => time() + (30 * 24 * 60 * 60),
        "path"     => "/",
        "secure"   => true,
        "httponly"  => true,
        "samesite" => "Strict",
    ]);
}

// === IN config.php (auto-login from remember cookie) ===

if (!is_logged_in() && isset($_COOKIE["remember_token"])) {
    $token = $_COOKIE["remember_token"];
    $tokenHash = hash("sha256", $token);

    // Look up the token hash in the database
    $tokens = json_decode(file_get_contents("data/tokens.json") ?: "[]", true);
    foreach ($tokens as $stored) {
        if (hash_equals($stored["token_hash"], $tokenHash)
            && $stored["expires"] > time()
        ) {
            // Token is valid! Log the user in
            $userId = $stored["user_id"];
            // Look up user by ID and populate session...
            session_regenerate_id(true);
            $_SESSION["user_id"] = $userId;
            // ...populate other session data from database...
            break;
        }
    }
}

⚠️ Security Notes for Remember Me

  • Store the hash of the token in your database, not the raw token — if the database leaks, the tokens are useless
  • Use hash_equals() for comparison to prevent timing attacks
  • Set an expiration — remember tokens shouldn't last forever
  • Invalidate on logout — delete the remember token from the database and the cookie
  • Use random_bytes() for cryptographically secure token generation

Flash Messages

A flash message is a one-time message stored in the session that displays on the next page load and then disappears. They're perfect for feedback after redirects — "Login successful!", "Item added to cart!", "Profile updated!"

How Flash Messages Work

<?php
session_start();

// === Setting a flash message (before a redirect) ===
$_SESSION["flash"] = [
    "type"    => "success",  // "success", "error", "warning", "info"
    "message" => "Your profile has been updated!",
];
header("Location: profile.php");
exit;

// === Displaying the flash message (on the next page) ===
// In profile.php:
session_start();

if (isset($_SESSION["flash"])) {
    $flash = $_SESSION["flash"];
    unset($_SESSION["flash"]); // Remove so it only shows once

    $type = htmlspecialchars($flash["type"]);
    $message = htmlspecialchars($flash["message"]);
    echo "<div class=\"alert alert-$type\">$message</div>";
}

Reusable Flash Message System

<?php
// flash_helpers.php — include this in your config

/**
 * Set a flash message.
 */
function set_flash(string $type, string $message): void {
    $_SESSION["flash_messages"][] = [
        "type"    => $type,
        "message" => $message,
    ];
}

/**
 * Get and clear all flash messages.
 */
function get_flash_messages(): array {
    $messages = $_SESSION["flash_messages"] ?? [];
    unset($_SESSION["flash_messages"]);
    return $messages;
}

/**
 * Render flash messages as HTML.
 */
function render_flash_messages(): string {
    $html = "";
    foreach (get_flash_messages() as $flash) {
        $type = htmlspecialchars($flash["type"]);
        $msg  = htmlspecialchars($flash["message"]);
        $html .= "<div class=\"alert alert-$type\">$msg</div>\n";
    }
    return $html;
}

// === Usage ===

// Setting (before redirect):
set_flash("success", "Item added to cart!");
set_flash("info", "Free shipping on orders over $50.");
header("Location: cart.php");
exit;

// Displaying (in the template/page):
echo render_flash_messages();
// Shows both messages, then they're gone on the next load

✅ The PRG Pattern

Post/Redirect/Get (PRG) is a web pattern where form submissions (POST) are followed by a redirect (302) to a GET page. This prevents duplicate submissions when the user refreshes. Flash messages are the perfect companion — set the message before the redirect, display it after.

Security Best Practices

1. Regenerate Session ID After Login

<?php
// ALWAYS regenerate the session ID after authentication
// This prevents "session fixation" attacks

session_start();

// After verifying username/password:
session_regenerate_id(true); // true = delete old session file
$_SESSION["user_id"] = $user["id"];

// What is session fixation?
// An attacker gives you a URL like: login.php?PHPSESSID=KNOWN_ID
// You log in, but the session ID is one the attacker chose
// Now the attacker has a valid session for your account!
// session_regenerate_id() gives you a new, random ID after login

2. Set Secure Cookie Flags

<?php
// Set these BEFORE session_start()

// HttpOnly — JavaScript can't read the session cookie (prevents XSS theft)
ini_set("session.cookie_httponly", 1);

// Secure — cookie only sent over HTTPS
ini_set("session.cookie_secure", 1);

// SameSite — prevents CSRF attacks
ini_set("session.cookie_samesite", "Strict");
// "Strict" = cookie never sent with cross-site requests
// "Lax"    = cookie sent for top-level navigations (links) but not forms/ajax
// "None"   = always sent (requires Secure flag) — rarely needed

// Use cookies only (never append session ID to URLs)
ini_set("session.use_only_cookies", 1);
ini_set("session.use_trans_sid", 0);

session_start();

3. Session Timeout

<?php
session_start();
define("SESSION_TIMEOUT", 30 * 60); // 30 minutes

if (isset($_SESSION["last_activity"])) {
    $elapsed = time() - $_SESSION["last_activity"];

    if ($elapsed > SESSION_TIMEOUT) {
        // Session has expired — log the user out
        $_SESSION = [];
        session_destroy();
        header("Location: login.php?timeout=1");
        exit;
    }
}

// Update last activity timestamp
$_SESSION["last_activity"] = time();

4. Password Security

<?php
// NEVER store plain text passwords!

// Hashing a password (during registration)
$password = "user_secret_123";
$hash = password_hash($password, PASSWORD_DEFAULT);
// $2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi

// Verifying a password (during login)
$submitted = $_POST["password"];
$storedHash = $user["password"]; // From database

if (password_verify($submitted, $storedHash)) {
    echo "Password is correct!";
} else {
    echo "Invalid password.";
}

// password_hash uses bcrypt by default:
// - Automatically generates a random salt
// - Includes the algorithm and cost in the hash
// - Future-proof: PASSWORD_DEFAULT may change to a stronger algorithm

// Check if a hash needs rehashing (after PHP upgrades the default)
if (password_needs_rehash($storedHash, PASSWORD_DEFAULT)) {
    $newHash = password_hash($submitted, PASSWORD_DEFAULT);
    // Update the hash in the database
}

Security Checklist

🔒 Session Security Checklist

  • session_regenerate_id(true) after login
  • session.cookie_httponly = 1 (blocks JavaScript access)
  • session.cookie_secure = 1 (HTTPS only)
  • session.cookie_samesite = "Strict" or "Lax"
  • session.use_only_cookies = 1 (no URL-based session IDs)
  • ✅ Implement session timeout for idle users
  • ✅ Use password_hash() and password_verify()
  • ✅ Generic error messages — "Invalid username or password" (don't say which is wrong)
  • ✅ Destroy session properly on logout (clear, delete cookie, destroy)

Hands-On Exercises

🏋️ Exercise 1: Page View Counter

Objective: Build a page that counts how many times the current visitor has viewed it using sessions, and separately tracks total all-time views using a file.

Instructions:

  1. Create counter.php with session_start()
  2. Increment $_SESSION["views"] on every visit
  3. Also increment a total count stored in data/total_views.txt
  4. Display: "You have visited this page X times. Total views: Y."
  5. Add a "Reset My Count" link that clears only the session counter
💡 Hint

Initialize $_SESSION["views"] to 0 if not set, then increment. For the file counter, use file_get_contents + file_put_contents with LOCK_EX. The reset link can use a query parameter: counter.php?reset=1.

✅ Solution
<?php
session_start();

$totalFile = __DIR__ . "/data/total_views.txt";

// Handle reset
if (isset($_GET["reset"])) {
    $_SESSION["views"] = 0;
    header("Location: counter.php");
    exit;
}

// Initialize session counter
if (!isset($_SESSION["views"])) {
    $_SESSION["views"] = 0;
}
$_SESSION["views"]++;

// Increment total file counter
if (!is_dir(dirname($totalFile))) {
    mkdir(dirname($totalFile), 0755, true);
}
$total = (int)(file_get_contents($totalFile) ?: 0);
$total++;
file_put_contents($totalFile, $total, LOCK_EX);
?>
<!DOCTYPE html>
<html>
<head><title>Page View Counter</title></head>
<body>
    <h1>Page View Counter</h1>
    <p>You have visited this page <strong><?= $_SESSION["views"] ?></strong> time(s).</p>
    <p>Total all-time views: <strong><?= $total ?></strong></p>
    <p><a href="counter.php">Refresh</a> | <a href="counter.php?reset=1">Reset My Count</a></p>
    <p><small>Session ID: <?= htmlspecialchars(session_id()) ?></small></p>
</body>
</html>

🏋️ Exercise 2: Theme Switcher with Cookies

Objective: Build a page where the user can choose a color theme (light, dark, blue) that persists across browser sessions using cookies.

Instructions:

  1. Create theme.php that reads a "theme" cookie (default: "light")
  2. Display three buttons/links to switch themes: Light, Dark, Blue
  3. When clicked, set the cookie to the chosen theme (expires in 365 days) and redirect back
  4. Apply the theme by changing the page's background color and text color based on the cookie value
  5. Add a "Reset to Default" button that deletes the cookie
💡 Hint

Use a query parameter like theme.php?set=dark to trigger the cookie change. Define an array of theme styles: ["light" => ["bg" => "#fff", "text" => "#333"], ...]. Set the cookie before output, then redirect to strip the query parameter.

✅ Solution
<?php
$themes = [
    "light" => ["bg" => "#ffffff", "text" => "#333333", "label" => "Light"],
    "dark"  => ["bg" => "#1a1a2e", "text" => "#e0e0e0", "label" => "Dark"],
    "blue"  => ["bg" => "#0a1628", "text" => "#a8d8ea", "label" => "Blue"],
];

// Handle theme change
if (isset($_GET["set"])) {
    $chosen = $_GET["set"];
    if ($chosen === "reset") {
        setcookie("theme", "", time() - 3600, "/");
    } elseif (isset($themes[$chosen])) {
        setcookie("theme", $chosen, time() + (365 * 24 * 60 * 60), "/");
    }
    header("Location: theme.php");
    exit;
}

// Read current theme
$current = $_COOKIE["theme"] ?? "light";
if (!isset($themes[$current])) $current = "light";
$style = $themes[$current];
?>
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>Theme Switcher</title>
    <style>
        body {
            background-color: <?= $style["bg"] ?>;
            color: <?= $style["text"] ?>;
            font-family: sans-serif;
            padding: 2rem;
            transition: background-color 0.3s, color 0.3s;
        }
        a { color: inherit; margin-right: 1rem; }
        .active { font-weight: bold; text-decoration: underline; }
    </style>
</head>
<body>
    <h1>Theme Switcher</h1>
    <p>Current theme: <strong><?= htmlspecialchars($themes[$current]["label"]) ?></strong></p>

    <nav>
        <?php foreach ($themes as $key => $t): ?>
            <a href="theme.php?set=<?= $key ?>"
               class="<?= $key === $current ? 'active' : '' ?>">
                <?= htmlspecialchars($t["label"]) ?>
            </a>
        <?php endforeach; ?>
        <a href="theme.php?set=reset">Reset to Default</a>
    </nav>

    <p>Close your browser and come back — the theme will persist!</p>
    <p><small>Cookie value: <?= htmlspecialchars($_COOKIE["theme"] ?? "(not set)") ?></small></p>
</body>
</html>

🎯 Quick Quiz

Question 1: Why must session_start() be called before any HTML output?

Question 2: Why should you call session_regenerate_id(true) after a successful login?

Question 3: A cookie you set with setcookie() is immediately available in $_COOKIE on the same request. True or false?

Question 4: What does the httponly cookie flag do?

Question 5: How do you properly destroy a PHP session?

Summary

🎉 Key Takeaways

  • HTTP is stateless — cookies and sessions add state on top of it
  • Cookies store data in the browser (up to ~4 KB, user-readable/modifiable) — use for preferences, not secrets
  • setcookie() sets a cookie (must come before output); $_COOKIE reads cookies from the previous request
  • Sessions store data on the server, linked via a session ID cookie — the standard for auth and sensitive data
  • session_start() must come before output and before any $_SESSION use
  • Login flow: verify credentials → session_regenerate_id(true) → populate $_SESSION → redirect
  • Logout flow: $_SESSION = [] → delete session cookie → session_destroy() → redirect
  • Remember me: store a hashed token in the database and the raw token in a long-lived cookie
  • Flash messages: one-time session data for post-redirect feedback
  • Security: httponly, secure, samesite flags, session regeneration, timeouts, password_hash()/password_verify()

📚 Additional Resources

🚀 What's Next?

With sessions and cookies mastered, you can manage user state across requests. Next up: Lesson 16: Object-Oriented PHP — Classes & Objects. We'll learn to organize code into classes with properties, methods, constructors, visibility modifiers, and static members — the foundation for modern PHP development.

🎉 Congratulations!

You can now build authenticated web applications with login/logout, persistent user preferences, and flash messages. These are the building blocks of every real PHP application!