<?php OOP: Zero to Hero Chapter 17 of 20

Chapter 17 ยท Databases & Forms

Sessions & Login

HTTP has amnesia: every page load is a brand-new conversation, and PHP forgets you completely between requests. Yet somehow websites remember you're logged in as you click around. The trick is called a session โ€” and it's the final skill we need before the project.

๐ŸŽŸ๏ธ
Real-world analogy

Think of a cloakroom at an event. You hand in your coat and receive a numbered ticket. The cloakroom keeps a shelf for each ticket number. Later, you show the ticket, and they instantly find your shelf. A session works the same way: PHP keeps a private shelf on the server for each visitor and gives the browser a ticket (a cookie with a random session ID). On every request the browser shows the ticket, and PHP fetches the right shelf into $_SESSION.

Session basics

page1.php<?php
session_start();                       // ALWAYS the first line, before any HTML output

$_SESSION["favourite"] = "nshima";     // put something on my shelf
echo "Saved! Now visit page2.php";
page2.php<?php
session_start();                       // reopen my shelf

echo "You told me earlier your favourite food is: " . $_SESSION["favourite"];
You told me earlier your favourite food is: nshima

Two different pages, two different requests โ€” and the value survived. Rules to burn in:

Storing passwords โ€” never, ever in plain text

Before building login we must store passwords correctly. If your database ever leaks (it happens to giants), plain-text passwords hand every account to the attacker โ€” including accounts reused on email and banking. The professional standard is hashing:

๐Ÿฅฃ
Real-world analogy

Hashing is like cooking. Turning eggs, flour and sugar into a cake is easy; turning the cake back into eggs is impossible. password_hash() cooks the password into an unrecognisable string. At login we don't "uncook" it โ€” we cook the newly typed password the same way and check whether the two cakes match, using password_verify().

// When creating a user (registration):
$hash = password_hash("secret123", PASSWORD_DEFAULT);
echo $hash;
// Store $hash in the database โ€” never the real password

// When logging in:
$typed = "secret123";
if (password_verify($typed, $hash)) {
    echo "<br>Correct password!";
} else {
    echo "<br>Wrong password.";
}
$2y$10$8HxT3mQ9kLpWvB1cRnEo4eXy7ZaJd0fGh2iUjKlMnOpQrStUvWxYi Correct password!
Forbidden forever

Never store plain passwords. Never use md5() or sha1() for passwords (old tutorials still show this โ€” they are crackable in seconds today). Only password_hash() / password_verify(). The hash column in your table should be VARCHAR(255) to fit current and future hash formats.

A users table

CREATE TABLE users (
    id INT AUTO_INCREMENT PRIMARY KEY,
    username VARCHAR(50) NOT NULL UNIQUE,
    password_hash VARCHAR(255) NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- Insert an admin user. Generate the hash with PHP first:
-- echo password_hash("admin123", PASSWORD_DEFAULT);
INSERT INTO users (username, password_hash)
VALUES ('admin', '$2y$10$PASTE_THE_HASH_YOU_GENERATED_HERE');

The complete login flow

login.php<?php
session_start();
require "Database.php";

$error = "";

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

    $pdo = Database::connect();
    $stmt = $pdo->prepare("SELECT * FROM users WHERE username = ?");
    $stmt->execute([$username]);
    $user = $stmt->fetch();                       // one row, or false if no such user

    if ($user && password_verify($password, $user["password_hash"])) {
        session_regenerate_id(true);              // fresh ticket after login (security habit)
        $_SESSION["user_id"]  = $user["id"];      // THE moment of logging in:
        $_SESSION["username"] = $user["username"];// remember who this shelf belongs to
        header("Location: dashboard.php");        // send the browser onward
        exit;                                     // always exit after a redirect
    } else {
        $error = "Wrong username or password.";   // same message for both cases โ€” on purpose!
    }
}
?>
<!DOCTYPE html>
<html>
<body>
    <h1>Login</h1>
    <?php if ($error): ?><p style="color:red"><?= $error ?></p><?php endif; ?>
    <form method="post">
        <label>Username: <input type="text" name="username"></label><br>
        <label>Password: <input type="password" name="password"></label><br>
        <button type="submit">Log in</button>
    </form>
</body>
</html>
Why "wrong username or password" and not which one?

If the message said "no such username", attackers could probe which usernames exist and then attack only the passwords. One vague message reveals nothing. Small detail, professional habit.

Protecting pages

Being "logged in" simply means your shelf contains a user_id. So a protected page just checks the shelf and evicts strangers. Put the check in one small file and require it at the top of every private page:

auth-check.php<?php
session_start();

if (!isset($_SESSION["user_id"])) {
    header("Location: login.php");
    exit;
}
dashboard.php<?php
require "auth-check.php";     // strangers never get past this line

echo "Welcome back, " . htmlspecialchars($_SESSION["username"]) . "!";

Logging out

Logging out = destroying the shelf and the ticket:

logout.php<?php
session_start();
$_SESSION = [];               // empty the shelf
session_destroy();            // throw the shelf away
header("Location: login.php");
exit;
โœ๏ธ Try it yourself

Build a tiny page counter: a page that increments $_SESSION["visits"] on every refresh and prints "You have visited this page N times." (Hint: ?? gives a starting value of 0 on the first visit.)

Show solution
<?php
session_start();

$_SESSION["visits"] = ($_SESSION["visits"] ?? 0) + 1;

echo "You have visited this page " . $_SESSION["visits"] . " times.";
You have visited this page 3 times.
Quick quiz

Where must session_start() appear?

Quick quiz

How does password_verify() check a password if hashes can't be reversed?

๐Ÿ”‘ Key points
  • Sessions defeat HTTP's amnesia: a server-side shelf per visitor, found via a ticket cookie.
  • session_start() first line, every session page; then $_SESSION is a normal array.
  • Passwords: password_hash() to store, password_verify() to check. Never plain text, never md5.
  • Login = verify, then store user_id in the session and redirect (with exit).
  • Protect pages with one shared auth-check.php; logout = empty + destroy the session.
  • Use one vague "wrong username or password" message โ€” don't help attackers probe.