Sessions

HTTP is stateless: every request is independent, and the server doesn't remember the previous one. Sessions fix that. PHP gives each visitor a random session ID, stored in a cookie, and keeps data for that visitor on the server in the $_SESSION array — perfect for logins, shopping carts and one-time messages.

Starting a session

Call session_start() at the top of every page that uses the session, before any output. Then read and write $_SESSION like a normal array; PHP saves it automatically at the end of the request:

PHP counter.php
<?php
session_start();

$_SESSION['visits'] = ($_SESSION['visits'] ?? 0) + 1;
$_SESSION['last_page'] = 'counter';

echo "You have viewed this page {$_SESSION['visits']} time(s).";
HTTP
GET /counter.php
GET /counter.php
GET /counter.php
Responses
> GET /counter.php
< Set-Cookie: PHPSESSID=…; path=/
You have viewed this page 1 time(s).
> GET /counter.php
You have viewed this page 2 time(s).
> GET /counter.php
You have viewed this page 3 time(s).

On the first visit, PHP creates a session and sends a PHPSESSID cookie containing a long random ID. The browser sends that cookie back with every later request, and PHP loads the matching data. The data itself never leaves the server — the visitor only holds the ID.

A shopping cart

Session data can be any serialisable value — arrays work well:

PHP cart.php
<?php
session_start();
header('Content-Type: text/plain');

$_SESSION['cart'] ??= [];
$prices = ['pen' => 1.20, 'notebook' => 3.50, 'stapler' => 8.90];

$action = $_POST['action'] ?? '';
$item = $_POST['item'] ?? '';

if ($action === 'add' && isset($prices[$item])) {
    $_SESSION['cart'][$item] = ($_SESSION['cart'][$item] ?? 0) + 1;
} elseif ($action === 'remove') {
    unset($_SESSION['cart'][$item]);
} elseif ($action === 'clear') {
    $_SESSION['cart'] = [];
}

$total = 0;
foreach ($_SESSION['cart'] as $name => $qty) {
    printf("%-9s x%d %6.2f\n", $name, $qty, $prices[$name] * $qty);
    $total += $prices[$name] * $qty;
}
printf("Total: %.2f\n", $total);
HTTP
POST /cart.php action=add&item=pen
POST /cart.php action=add&item=notebook
POST /cart.php action=add&item=pen
POST /cart.php action=remove&item=notebook
Responses
> POST /cart.php
pen       x1   1.20
Total: 1.20
> POST /cart.php
pen       x1   1.20
notebook  x1   3.50
Total: 4.70
> POST /cart.php
pen       x2   2.40
notebook  x1   3.50
Total: 5.90
> POST /cart.php
pen       x2   2.40
Total: 2.40

Logging in and out

A login stores the user's ID in the session after checking their password. Two security details matter:

  • Call session_regenerate_id(true) right after logging in. It gives the session a new ID, so an attacker who planted or guessed the old one (session fixation) gains nothing.
  • On logout, clear the data and destroy the session.
PHP login.php
<?php
session_start();
header('Content-Type: text/plain');

// In a real app these come from a database; the hash was made with password_hash('secret123', PASSWORD_DEFAULT)
$users = ['ana' => password_hash('secret123', PASSWORD_DEFAULT)];

$action = $_GET['action'] ?? 'status';

if ($action === 'login' && $_SERVER['REQUEST_METHOD'] === 'POST') {
    $user = $_POST['user'] ?? '';
    $hash = $users[$user] ?? null;
    if ($hash !== null && password_verify($_POST['password'] ?? '', $hash)) {
        session_regenerate_id(true);
        $_SESSION['user'] = $user;
        echo "Logged in as $user\n";
    } else {
        echo "Invalid username or password\n";
    }
} elseif ($action === 'logout') {
    $_SESSION = [];
    session_destroy();
    echo "Logged out\n";
} else {
    echo isset($_SESSION['user']) ? "Signed in: {$_SESSION['user']}\n" : "Not signed in\n";
}
HTTP
GET /login.php
POST /login.php?action=login user=ana&password=wrong
POST /login.php?action=login user=ana&password=secret123
GET /login.php
GET /login.php?action=logout
GET /login.php
Responses
> GET /login.php
Not signed in
> POST /login.php?action=login
Invalid username or password
> POST /login.php?action=login
Logged in as ana
> GET /login.php
Signed in: ana
> GET /login.php?action=logout
Logged out
> GET /login.php
Not signed in

Passwords are never stored or compared as plain text — password_hash and password_verify are explained in lesson 59. Note the error message doesn't reveal whether the username or the password was wrong.

Flash messages

A "flash" message is stored in the session, shown on the next page, and then deleted. It's the standard way to say "Saved!" after a Post/Redirect/Get:

PHP profile.php
<?php
session_start();

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    // ...save the profile...
    $_SESSION['flash'] = 'Your profile was updated.';
    header('Location: /profile.php');
    exit;
}

$flash = $_SESSION['flash'] ?? null;
unset($_SESSION['flash']);
?>
<?php if ($flash): ?><div class="flash"><?= htmlspecialchars($flash) ?></div><?php endif; ?>
<h1>Your profile</h1>
HTTP
POST /profile.php name=Ana
GET /profile.php
GET /profile.php
Responses
> POST /profile.php
< 302 Found
< Location: /profile.php
> GET /profile.php
<div class="flash">Your profile was updated.</div><h1>Your profile</h1>
> GET /profile.php
<h1>Your profile</h1>

The message appears exactly once.

Session configuration and security

  • Sessions are only as secure as their cookie. Serve your site over HTTPS and set the cookie flags secure, httponly and samesite — pass them to session_start(['cookie_secure' => true, 'cookie_httponly' => true, 'cookie_samesite' => 'Lax']) or set them in php.ini.
  • Keep sessions small: store IDs, not whole database records.
  • session_start() locks the session until the request ends, so two simultaneous requests from the same user wait for each other. Call session_write_close() early in slow scripts that no longer need to write.
  • By default, session data is stored in files on the server. Applications running on several servers store sessions in a shared place such as Redis or a database.

Try it yourself

Build a "recently viewed products" list that remembers the last three products a visitor opened.

Try it yourself · recent.php
Edit the code, copy it into recent.php, then start PHP's built-in web server with php -S localhost:8000 in that folder and open http://localhost:8000/recent.php (PHP 8.5). How to install PHP