Himanshu SharmaSoftware engineer
Let’s talk

PHP · Lesson 37 of 54

Sessions

Keep data on the server between requests with PHP sessions, build a login and a shopping cart, regenerate the session ID, and log out properly.

14 minPHP on the webUpdated 22 October 2026

Awaiting technical review. This lesson is readable but not yet listed in search engines.

By the end of this lessonUse sessions to remember a signed-in user and a cart between requests, securely.

Data that stays on the server

Cookies remember things in the visitor's browser, where they can be read and changed. A session keeps the data on the server instead. The browser holds only a long random session ID in a cookie (called PHPSESSID by default), and PHP uses that ID to find the visitor's data on each request. Because the visitor never sees the data, a session is the right place for "who is signed in" and "what is in the cart".

Starting a session

session_start() reads the session ID cookie, loads the data into $_SESSION, or starts a new session if there is none. It sends a cookie, so like setcookie() it must run before any output. After that, $_SESSION is an ordinary array that PHP saves automatically at the end of the request:

php
<?php
session_start();

$_SESSION['views'] = ($_SESSION['views'] ?? 0) + 1;
echo "You have viewed this page {$_SESSION['views']} time(s) in this session.";

The first request starts a session. The response below includes its status line and headers, so you can see the cookie PHP sends (the real ID is a long random string, replaced here):

http
GET /counter.php
result
HTTP/1.1 200 OK
Set-Cookie: PHPSESSID=<random id>; path=/

You have viewed this page 1 time(s) in this session.

The browser sends the cookie back with the next request, and the same data is loaded:

http
GET /counter.php
result
You have viewed this page 2 time(s) in this session.

A shopping cart

A cart is a natural session example: the visitor adds products on one page and sees them on another. Store product IDs and quantities, not prices, and look up the current prices when you show the cart:

php
<?php
session_start();

$catalogue = [1 => ['Mechanical Keyboard', 89.5], 2 => ['Ceramic Mug Set', 18.75], 3 => ['Water Bottle', 12.0]];
$_SESSION['cart'] ??= [];

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $id = filter_var($_POST['product'] ?? '', FILTER_VALIDATE_INT);
    if ($id !== false && isset($catalogue[$id])) {
        $_SESSION['cart'][$id] = ($_SESSION['cart'][$id] ?? 0) + 1;
    }
    header('Location: /cart.php', true, 303);
    exit;
}

$total = 0;
foreach ($_SESSION['cart'] as $id => $qty) {
    [$name, $price] = $catalogue[$id];
    $total += $qty * $price;
    echo "$qty x $name\n";
}
echo "Total: " . number_format($total, 2) . "\n";

Adding a keyboard and two mugs, then looking at the cart:

http
POST /cart.php
Content-Type: application/x-www-form-urlencoded

product=1
result
HTTP/1.1 303 See Other
Location: /cart.php
http
POST /cart.php
Content-Type: application/x-www-form-urlencoded

product=2
http
POST /cart.php
Content-Type: application/x-www-form-urlencoded

product=2
http
GET /cart.php
result
1 x Mechanical Keyboard
2 x Ceramic Mug Set
Total: 127.00

The cart survived across four requests, and the redirect after each POST follows the Post/Redirect/Get pattern from Superglobals, headers and redirects.

Signing in and out

A login stores the user's ID in the session after checking the password. Two details make it secure: regenerate the session ID on login, and destroy the session properly on logout. (How to check a password safely is in Storing passwords safely; here a fixed hash stands in for the database.)

php
<?php
session_start();

$users = ['asha@example.com' => ['id' => 7, 'hash' => password_hash('correct horse', PASSWORD_DEFAULT)]];

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $email = strtolower(trim($_POST['email'] ?? ''));
    $user = $users[$email] ?? null;

    if ($user && password_verify($_POST['password'] ?? '', $user['hash'])) {
        session_regenerate_id(true);
        $_SESSION['user_id'] = $user['id'];
        header('Location: /account.php', true, 303);
        exit;
    }
    echo "Email or password is wrong.";
    exit;
}
php
<?php
session_start();

if (!isset($_SESSION['user_id'])) {
    header('Location: /login.php', true, 302);
    exit;
}
echo "Signed in as user {$_SESSION['user_id']}.";
php
<?php
session_start();

$_SESSION = [];
$params = session_get_cookie_params();
setcookie(session_name(), '', ['expires' => time() - 3600, 'path' => $params['path']]);
session_destroy();

echo "You are signed out.";

Try the account page while signed out, then sign in, then visit it again:

http
GET /account.php
result
HTTP/1.1 302 Found
Location: /login.php
http
POST /login.php
Content-Type: application/x-www-form-urlencoded

email=asha%40example.com&password=wrong
result
Email or password is wrong.
http
POST /login.php
Content-Type: application/x-www-form-urlencoded

email=asha%40example.com&password=correct+horse
http
GET /account.php
result
Signed in as user 7.
http
GET /logout.php
result
You are signed out.
http
GET /account.php
result
HTTP/1.1 302 Found
Location: /login.php
Set-Cookie: PHPSESSID=<random id>; path=/

Only the last response sets a new PHPSESSID cookie. Before that, the browser already had a session from the earlier examples; after logout that session was destroyed, so PHP started a fresh, empty one.

Why session_regenerate_id(true)? If an attacker can make a victim use a session ID the attacker already knows (for example through a link), and the ID stays the same after login, the attacker is now signed in as the victim. Giving the session a new ID at login makes the old one worthless. This attack is called session fixation.

The error message says "email or password", not which one was wrong, so attackers cannot use the login form to find out which email addresses have accounts.

Session settings

A few settings in php.ini (or passed to session_start() as options) make sessions safer on a live site:

php
<?php
session_start([
    'cookie_secure' => true,     // HTTPS only
    'cookie_httponly' => true,   // hidden from JavaScript
    'cookie_samesite' => 'Lax',
    'use_strict_mode' => true,   // reject session IDs the server did not create
]);

Sessions expire after a period without activity (session.gc_maxlifetime, 24 minutes by default). By default PHP stores sessions as files on the server; frameworks often store them in a database or Redis so they work across several servers.

Common mistakes

  • Calling session_start() after output, or forgetting it on a page that reads $_SESSION.
  • Storing too much in the session, such as whole database rows that then go stale. Store IDs.
  • Not regenerating the ID at login.
  • Logging out with only unset($_SESSION['user_id']) and leaving the rest of the session behind.

Exercise

Add a "remove one" action to the cart: a POST to /cart.php with remove=ID should decrease that product's quantity by one and delete it from the cart when it reaches zero. Write only the extra code that goes inside the if ($_SERVER['REQUEST_METHOD'] === 'POST') block.

Show one possible answer
php
$remove = filter_var($_POST['remove'] ?? '', FILTER_VALIDATE_INT);
if ($remove !== false && isset($_SESSION['cart'][$remove])) {
    $_SESSION['cart'][$remove]--;
    if ($_SESSION['cart'][$remove] <= 0) {
        unset($_SESSION['cart'][$remove]);
    }
}

Put it before the redirect, so the visitor lands on the updated cart.

Key points

  • session_start() loads the visitor's session; $_SESSION then reads and writes data stored on the server.
  • Only a random session ID travels in a cookie (PHPSESSID), so visitors cannot change the data itself.
  • Call session_regenerate_id(true) after a login to prevent session fixation.
  • Log out by clearing $_SESSION, deleting the session cookie and calling session_destroy().

Check yourself

1. Where is $_SESSION data stored?
2. What must you call before using $_SESSION?
3. Why call session_regenerate_id(true) after a successful login?
4. Which is a good thing to keep in a session after login?