LEARN TO CODE | PHP | 02 PAGES AND FORMS
02.4 Sessions and cookies
Explain how requests share session state, distinguish session data from cookie values and build a small remembered-preference workflow.
01 | WHY ORDINARY VARIABLES ARE NOT ENOUGH
Continue using the PHP development server from 01.1. Create the files below inside one practice folder, preserving their filenames and subfolders. In VS Code open that folder, then start
php -S 127.0.0.1:8000
in its terminal and open the named page through HTTP.
These are complete local examples for PHP 8. Text inputs and output are treated deliberately, but the examples are not a login system, a production deployment or a saved registration service. Where a page previews a submission only, nothing is sent or stored.
A variable created in favourite.php does not automatically exist when the browser requests another page. HTTP requests are separate exchanges.
A session provides a way for the server to associate several requests with the same stored state. A cookie is a value the browser stores and sends back on matching requests.
The favourite example remembers a club choice. It does not authenticate a user or establish their identity.
02 | FOLLOW THE SESSION ROUND TRIP
| Step | What happens |
|---|---|
| First request | PHP starts a session and sends a session-ID cookie where needed |
| Server storage | A favourite is stored in session data associated with that ID |
| Later request | Browser sends the matching session cookie |
| session_start | PHP loads the data associated with the session |
| Response | The page displays the stored favourite |
By default in this learning setup, PHP’s session handler stores session data on the server, typically in files. The cookie normally contains the identifier, not the whole favourite array. Other configured handlers can store session data elsewhere.
A session is not necessarily a login, a single browser tab or an individual person. Tabs normally share cookies for the same site; a separate private browser context can have another session.
03 | START BEFORE SENDING OUTPUT
session_start and setcookie can need response headers. A header must be sent before response-body output. Call session setup before HTML, echo, and any include that prints content.
Whitespace or a byte-order mark before an opening PHP tag can also create unwanted output. Put the opening PHP tag at the start of the bootstrap file and omit its final closing tag.
Use helpers.php from 02.1, then add the shared bootstrap below to the same folder.
session-bootstrap.php
<?php
require_once __DIR__ . '/helpers.php';
// LOCAL HTTP PRACTICE ONLY: use secure => true on an HTTPS deployment.
ini_set('session.use_strict_mode', '1');
ini_set('session.use_only_cookies', '1');
session_set_cookie_params([
'lifetime' => 0,
'path' => '/',
'secure' => false,
'httponly' => true,
'samesite' => 'Lax',
]);
if (!session_start()) {
throw new RuntimeException('Could not start a session.');
}
if (!isset($_SESSION['csrf'])) {
$_SESSION['csrf'] = bin2hex(random_bytes(32));
}
function validPostToken(): bool
{
$token = $_POST['csrf'] ?? null;
return is_string($token) && hash_equals($_SESSION['csrf'], $token);
}
04 | UNDERSTAND THE BOOTSTRAP
The cookie settings apply before session_start. HttpOnly prevents ordinary JavaScript access to that cookie; SameSite=Lax limits some cross-site sending. These are useful properties, not complete protection against every attack.
secure is false only because the example uses local HTTP. For a deployed HTTPS site, set Secure appropriately and use HTTPS consistently. The default lifetime 0 requests a session cookie, but browser restore behaviour and server expiry mean “close the browser” is not a dependable logout policy.
Strict session mode and cookie-only session IDs are configured deliberately. Actual login systems also need authentication, authorisation, ID regeneration at authentication changes and a considered expiry/logout process.
05 | A FORM TOKEN PROTECTS THE STATE CHANGE
The bootstrap creates an unpredictable CSRF token in the session. The form includes a copy in a hidden input. On POST, PHP compares the submitted token with the stored token using hash_equals.
This helps reject a state-changing request that did not carry the session’s expected token. A hidden input is not inherently trusted: the protection comes from the server-held token and its unpredictability.
GET displays data here; POST saves or clears the choice. A token does not validate the club option or establish user permission, so those checks remain separate.
06 | SAVE AND CLEAR A SESSION FAVOURITE
favourite.php
<?php
require_once __DIR__ . '/session-bootstrap.php';
$clubs = ['robotics' => 'Robotics', 'art' => 'Art & Design'];
$error = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if (!validPostToken()) {
http_response_code(403);
$error = 'The form token was missing or invalid. Reload and try again.';
} else {
$action = $_POST['action'] ?? '';
if ($action === 'clear') {
unset($_SESSION['favourite']);
header('Location: favourite.php', true, 303);
exit;
}
$choice = $_POST['club'] ?? '';
if ($action !== 'save' || !is_string($choice) || !array_key_exists($choice, $clubs)) {
$error = 'Choose a listed club.';
} else {
$_SESSION['favourite'] = $choice;
header('Location: favourite.php', true, 303);
exit;
}
}
}
$key = $_SESSION['favourite'] ?? '';
$label = is_string($key) && isset($clubs[$key]) ? $clubs[$key] : 'Nothing selected';
?>
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><title>Session favourite</title></head>
<body>
<h1>Session favourite</h1>
<p>Current favourite: <?= escapeHtml($label) ?></p>
<?php if ($error !== ''): ?><p role="alert"><?= escapeHtml($error) ?></p><?php endif; ?>
<form action="favourite.php" method="post">
<input type="hidden" name="csrf" value="<?= escapeHtml($_SESSION['csrf']) ?>">
<input type="hidden" name="action" value="save">
<label for="club">Favourite club : </label>
<select id="club" name="club">
<?php foreach ($clubs as $value => $text): ?>
<option value="<?= escapeHtml($value) ?>" <?= $key === $value ? 'selected' : '' ?>><?= escapeHtml($text) ?></option>
<?php endforeach; ?>
</select>
<button type="submit">Remember</button>
</form>
<form action="favourite.php" method="post">
<input type="hidden" name="csrf" value="<?= escapeHtml($_SESSION['csrf']) ?>">
<input type="hidden" name="action" value="clear">
<button type="submit">Clear favourite</button>
</form>
<p><a href="session-summary.php">Open the second session page</a></p>
</body>
</html>
The clear action unsets only favourite; it does not claim to destroy the entire session or log out a real user. The save action validates a known club key before storing it.
Both successful changes return a 303 Location redirect and exit immediately. The next request is GET, so refreshing it does not repeat the original POST action.
07 | READ THE CHOICE ON ANOTHER PAGE
session-summary.php
<?php
require_once __DIR__ . '/session-bootstrap.php';
$clubs = ['robotics' => 'Robotics', 'art' => 'Art & Design'];
$key = $_SESSION['favourite'] ?? '';
$label = is_string($key) && isset($clubs[$key]) ? $clubs[$key] : 'Nothing selected';
?>
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><title>Session summary</title></head>
<body>
<h1>Another page, the same session</h1>
<p>Current favourite: <?= escapeHtml($label) ?></p>
<p><a href="favourite.php">Change favourite</a></p>
</body>
</html>
Open favourite.php, choose Art & Design and press Remember. Follow the second-page link: the choice should still appear because the same session cookie connects the requests.
Opening this page does not pass the club in the URL. The page reads $_SESSION. Clear the favourite, return to the summary and confirm the fallback text appears.
08 | A DIRECT COOKIE IS DIFFERENT
preference.php
<?php
require_once __DIR__ . '/session-bootstrap.php';
$allowed = ['compact', 'comfortable'];
$incoming = $_COOKIE['view_mode'] ?? '';
$mode = is_string($incoming) && in_array($incoming, $allowed, true) ? $incoming : 'comfortable';
$error = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$choice = $_POST['mode'] ?? '';
if (!validPostToken()) {
http_response_code(403);
$error = 'Invalid form token.';
} elseif (!is_string($choice) || !in_array($choice, $allowed, true)) {
$error = 'Choose a listed mode.';
} else {
setcookie('view_mode', $choice, [
'expires' => time() + 86400 * 7,
'path' => '/',
'secure' => false, // Local HTTP only; true for HTTPS deployment.
'httponly' => true,
'samesite' => 'Lax',
]);
header('Location: preference.php', true, 303);
exit;
}
}
?>
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><title>Cookie preference</title></head>
<body>
<h1>Cookie preference</h1>
<p>Current mode: <?= escapeHtml($mode) ?></p>
<p>This example displays the preference; it does not apply a CSS layout.</p>
<?php if ($error !== ''): ?><p role="alert"><?= escapeHtml($error) ?></p><?php endif; ?>
<form action="preference.php" method="post">
<input type="hidden" name="csrf" value="<?= escapeHtml($_SESSION['csrf']) ?>">
<label for="mode">View mode : </label>
<select id="mode" name="mode">
<?php foreach ($allowed as $value): ?>
<option value="<?= escapeHtml($value) ?>" <?= $mode === $value ? 'selected' : '' ?>><?= escapeHtml($value) ?></option>
<?php endforeach; ?>
</select>
<button type="submit">Save preference</button>
</form>
</body>
</html>
Here view_mode itself is stored in the browser cookie, not only on the server. PHP reads the incoming cookie, checks its value against an allow-list and falls back to comfortable if it is missing or unexpected.
setcookie asks the browser to store a value in the response. It does not modify the current request’s $_COOKIE array. The redirect creates a later request on which the browser can send the new value.
The seven-day expiry is a requested lifetime, not a promise of permanent storage. The browser can delete or block cookies. This demo displays a label rather than applying an actual CSS layout.
09 | DEBUG THE CONNECTION BETWEEN REQUESTS
Use one hostname consistently: 127.0.0.1 and localhost have different cookie host scopes. Check that cookies are allowed and that session storage is writable by PHP.
Inspect cookies in the browser’s developer tools to see the session-ID cookie and the separate view_mode value. Do not share session IDs or print them as ordinary page content.
A direct cookie is client-held input and can be changed. Never grant access just because a cookie says role=admin. Session data can support a real authenticated workflow, but this exercise does not implement one.
10 | TEST ISOLATION AND FORM FAILURE
TRY IT YOURSELF
Save a favourite in one browser tab, read it in another and compare with a fresh private browser window. Then submit a state change without a valid form token.
Reveal guidance
Normal tabs usually share the site’s session. A separate private context starts without the existing cookie and should show no favourite. A missing or incorrect token gives a 403 response without changing the stored choice. Test with the same hostname and server.
Next, complete the challenges before moving to files, JSON and database connections.
KEEP LEARNING
Previous page 02 Pages and forms: challenges
Official references: Includes and control structures · POST values · HTML escaping · Sessions · Cookies