1. Sessions
HTTP is stateless — by default, PHP has no way to know that two requests came from the same visitor. A session fixes this: PHP generates a unique session ID, sends it to the browser as a cookie, and stores server-side data keyed by that ID:
<?php
declare(strict_types=1);
session_start(); // must run before any output
$_SESSION['visits'] = ($_SESSION['visits'] ?? 0) + 1;
$_SESSION['user_id'] = 42;
echo "You've visited this page {$_SESSION['visits']} times.";
session_start() either resumes an existing session (by reading the
session cookie from the request) or begins a new one. Once called,
$_SESSION behaves like a regular associative array, except its
contents survive between requests — PHP stores them server-side (by default, as a
file in its session save path) and only sends the ID itself to the browser.
session_start() must come before any output
It sends an HTTP header to set the session cookie, and headers must be sent before any HTML, whitespace, or even a stray blank line before <?php. "Headers already sent" is one of the most common early PHP errors — it means something printed before session_start() ran.
To end a session — on logout, for example — clear the data and destroy it:
<?php
session_start();
$_SESSION = [];
session_destroy();
setcookie(session_name(), '', time() - 3600); // expire the session cookie
3. Reading & Writing Files
PHP's filesystem functions cover reading, writing and appending. The simplest
cases use file_get_contents() and file_put_contents():
<?php
declare(strict_types=1);
$logPath = __DIR__ . '/storage/app.log';
// Write (overwrite)
file_put_contents($logPath, "Server started\n");
// Append
file_put_contents($logPath, date('c') . " - request handled\n", FILE_APPEND);
// Read the whole file
$contents = file_get_contents($logPath);
// Read line by line without loading the whole file into memory
$handle = fopen($logPath, 'r');
while (($line = fgets($handle)) !== false) {
echo trim($line) . "\n";
}
fclose($handle);
file_get_contents()/file_put_contents() are simplest for
small files. For large files, fopen()/fgets()/
fclose() stream line by line instead of loading everything into
memory at once.
file_put_contents() returns false on failure (permissions, disk full, missing directory) rather than throwing. Check the return value, or wrap the call site in error handling — a silently-failed write is a nasty bug to track down later.
4. File Uploads
A form with enctype="multipart/form-data" can include file inputs;
PHP populates $_FILES with metadata about each uploaded file:
<?php
declare(strict_types=1);
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['avatar'])) {
$file = $_FILES['avatar'];
if ($file['error'] !== UPLOAD_ERR_OK) {
die('Upload failed with error code ' . $file['error']);
}
$allowedTypes = ['image/jpeg', 'image/png', 'image/webp'];
$mimeType = mime_content_type($file['tmp_name']); // check the real content, not the filename
if (!in_array($mimeType, $allowedTypes, true)) {
die('Only JPEG, PNG or WebP images are allowed.');
}
if ($file['size'] > 2 * 1024 * 1024) { // 2MB
die('File too large.');
}
$extension = pathinfo($file['name'], PATHINFO_EXTENSION);
$safeName = bin2hex(random_bytes(16)) . '.' . $extension; // never trust the original filename
$destination = __DIR__ . '/storage/uploads/' . $safeName;
move_uploaded_file($file['tmp_name'], $destination);
echo "Uploaded as {$safeName}";
}
?>
<form method="post" enctype="multipart/form-data">
<input type="file" name="avatar" accept="image/*">
<button type="submit">Upload</button>
</form>
move_uploaded_file() is the correct way to relocate an uploaded
file — it verifies the file actually came from a genuine upload before moving it,
unlike a plain rename(). Checking the real MIME type with
mime_content_type() (which inspects file content) matters because the
client-supplied $file['type'] and original filename are both fully
attacker-controlled and prove nothing on their own.
5. A Session-Gated Page
Combining this week's pieces: a login form that sets a session flag, and a protected page that checks it before rendering anything:
<?php
declare(strict_types=1);
session_start();
if (empty($_SESSION['logged_in'])) {
header('Location: /login.php');
exit; // always exit after a redirect -- code below would still run otherwise
}
echo "Welcome back, user #{$_SESSION['user_id']}!";
This page checks a session flag, but nothing here verifies a password yet — that needs password_hash()/password_verify() and, realistically, a database of users, both covered in Week 8 once MySQL is in place. Right now the goal is understanding the session mechanics: setting a flag on login, checking it on every protected page, and clearing it on logout.
6. Hands-on Exercise
Build a session-gated note-taking app
Combine sessions, flash messages and file I/O into a small, working app.
Requirements:
- Create
login.phpwith a form that accepts any username; on submit, set$_SESSION['logged_in'] = trueand$_SESSION['username'], then redirect tonotes.php. notes.phpmust redirect back tologin.phpif$_SESSION['logged_in']isn't set.- On
notes.php, accept a POST'd note and append it (with a timestamp) to a per-user file atstorage/notes/{username}.txt, then set a flash message "Note saved!" and redirect back to itself (the POST-redirect-GET pattern, avoiding a duplicate save on refresh). - Display the flash message once if present, then list every previously saved note read back from that user's file.
- Add a
logout.phpthat destroys the session and redirects tologin.php.
Redirecting after a POST before rendering anything (rather than rendering the result directly) is the "POST-redirect-GET" pattern — it's exactly why refreshing after a normal form submission doesn't usually resubmit the form.
7. Knowledge Check
Four quick questions. Expand each to check your answer.
Q1
Where does the actual data in $_SESSION get stored?
Where does the actual data in $_SESSION get stored?
Server-side — by default as a file in PHP's session save path. Only the session ID (a random token) is sent to the browser as a cookie. This matters for security: the browser never holds the session's actual contents, only a reference to them, so a user can't read or tamper with their own session data directly.
Q2
Why must session_start() run before any HTML or other output?
Why must session_start() run before any HTML or other output?
It works by sending an HTTP Set-Cookie header, and HTTP headers must be sent before the response body. Any output before it — even a stray blank line outside <?php ?> tags — counts as the start of the body and triggers a "headers already sent" error.
Q3
Why check the uploaded file's real MIME type with mime_content_type() instead of trusting $_FILES['avatar']['type']?
Why check the uploaded file's real MIME type with mime_content_type() instead of trusting $_FILES['avatar']['type']?
$_FILES[...]['type'] is read directly from a header the client sent — a browser or script uploading a malicious file can set it to anything, including image/png on a file that isn't actually an image. mime_content_type() inspects the file's actual bytes on the server, which is far harder to spoof.
Q4
Why does protected.php call exit; immediately after header('Location: ...')?
Why does protected.php call exit; immediately after header('Location: ...')?
header() only queues an HTTP header — it does not stop script execution. Without exit, every line after the redirect still runs on the server, meaning the "protected" content below it would still be computed (and, if there were any output before the header call, potentially even sent) despite the redirect. exit guarantees nothing further executes.