Chapter 15 ยท Databases & Forms
Talking to MySQL with PDO
Now the two worlds meet: our PHP objects will speak to our MySQL tables. The bridge is called PDO (PHP Data Objects) โ an object-oriented toolkit built into PHP. Everything you've learned about classes suddenly pays off.
PDO is a telephone line to the filing clerk. First you dial and identify yourself (connect with host, username, password). Then you make requests down the line (SQL queries) and the clerk reads back the matching record cards (rows). If the line is dead or you ask something impossible, the clerk raises an alarm โ an exception, which you already know how to catch!
Connecting
connect.php<?php
$host = "localhost";
$dbname = "school_db"; // from last chapter
$username = "root"; // XAMPP's default user
$password = ""; // XAMPP's default password is empty
try {
$pdo = new PDO(
"mysql:host=$host;dbname=$dbname;charset=utf8mb4",
$username,
$password
);
// Make PDO throw exceptions when things go wrong (always do this!)
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
// Make results come back as associative arrays by default
$pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
echo "Connected successfully!";
} catch (PDOException $e) {
die("Connection failed: " . $e->getMessage());
}
Look how many friends from earlier chapters appear: new PDO(...) is a constructor
taking arguments; $pdo is an object; setAttribute() is a method call;
PDO::ATTR_ERRMODE is a class constant; PDOException is a custom
exception. You already speak this language.
Reading rows โ query and fetch
// Run a SELECT and get a "statement" object holding the results
$stmt = $pdo->query("SELECT * FROM students ORDER BY full_name");
// fetchAll() = give me every row, as an array of associative arrays
$students = $stmt->fetchAll();
foreach ($students as $row) {
echo $row["full_name"] . " โ " . $row["program"] . "<br>";
}
Each row arrives as an associative array whose keys are the column names โ exactly the shape
from Chapter 02. fetch() (singular) returns just one row; fetchAll()
returns them all.
โ ๏ธ The most important security lesson in this course
Suppose a search box lets users type a name, and we build the query by gluing strings:
// โ ๏ธ NEVER DO THIS โ ๏ธ
$name = $_GET["name"]; // whatever the user typed
$stmt = $pdo->query("SELECT * FROM students WHERE full_name = '$name'");
A normal user types Chanda. An attacker types:
x'; DELETE FROM students; --
Glued into your string, that becomes two commands โ the second one deletes your entire students table. This attack is called SQL injection, and it has destroyed real companies. The user typed code, and we foolishly executed it.
The cure: prepared statements
A prepared statement is a printed form with blanks. You first hand the clerk the form: "SELECT students WHERE name = ". The clerk reads and approves the structure once. Then you fill the blank. Whatever is written in a blank is treated as pure data, never as instructions โ even if someone writes "DELETE everything" in the blank, the clerk just searches for a student literally named "DELETE everything" and finds nobody.
// โ
ALWAYS DO THIS โ
$stmt = $pdo->prepare("SELECT * FROM students WHERE full_name = ?");
$stmt->execute([$name]); // the value fills the ? blank โ safely
$student = $stmt->fetch();
// Multiple blanks fill in order:
$stmt = $pdo->prepare("SELECT * FROM students WHERE program = ? AND fee_balance > ?");
$stmt->execute(["BSc Computing", 5000]);
// Or use named blanks โ clearer when there are many:
$stmt = $pdo->prepare(
"INSERT INTO students (student_number, full_name, program, fee_balance)
VALUES (:number, :name, :program, :balance)"
);
$stmt->execute([
"number" => "LGU2026-005",
"name" => "Natasha Zulu",
"program" => "BSc Computing",
"balance" => 5500,
]);
echo "New student id: " . $pdo->lastInsertId();
Any value that comes from a user (form fields, URL parameters, cookies โ anything) goes into SQL only through a prepared statement's blanks. No exceptions, ever, for your entire career. Table and column names can't be parameterised โ those you write yourself, never from user input.
UPDATE and DELETE the same way
// Update
$stmt = $pdo->prepare("UPDATE students SET fee_balance = ? WHERE id = ?");
$stmt->execute([3500, 1]);
echo $stmt->rowCount() . " row(s) updated.<br>";
// Delete
$stmt = $pdo->prepare("DELETE FROM students WHERE id = ?");
$stmt->execute([5]);
echo $stmt->rowCount() . " row(s) deleted.";
Wrapping the connection in a class
Typing connection code on every page is repetition โ and you know how OOP feels about
repetition. Let's build a small Database class we'll reuse in the final project:
Database.php<?php
class Database {
private static ?PDO $pdo = null; // one shared connection (remember static?)
public static function connect(): PDO {
if (self::$pdo === null) { // connect only the first time
try {
self::$pdo = new PDO(
"mysql:host=localhost;dbname=school_db;charset=utf8mb4",
"root",
""
);
self::$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
self::$pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
} catch (PDOException $e) {
die("Database connection failed: " . $e->getMessage());
}
}
return self::$pdo;
}
}
any-page.php<?php
require "Database.php";
$pdo = Database::connect();
$students = $pdo->query("SELECT * FROM students")->fetchAll();
echo count($students) . " students found.";
A static method, a private static property, lazy connection, exception handling โ every chapter of this course is inside those 20 lines. (This "only ever one instance" pattern even has a famous name: the Singleton.)
Using the Database class, write a page that uses a prepared statement to fetch
all students whose fee_balance is greater than 5000 and prints each name with the
balance.
Show solution
<?php
require "Database.php";
$pdo = Database::connect();
$stmt = $pdo->prepare("SELECT full_name, fee_balance FROM students WHERE fee_balance > ?");
$stmt->execute([5000]);
foreach ($stmt->fetchAll() as $row) {
echo $row["full_name"] . " owes K" . $row["fee_balance"] . "<br>";
}
Why must user input always go through prepared statements?
What's the difference between fetch() and fetchAll()?
- PDO is PHP's object-oriented bridge to MySQL:
new PDO(dsn, user, pass). - Always set
ERRMODE_EXCEPTIONso failures throw catchable exceptions. - Rows arrive as associative arrays:
$row["full_name"]. - SQL injection = user input executed as SQL. It destroys systems.
- The cure:
prepare()with?or:namedblanks, thenexecute([values]). Every user value, every time, forever. lastInsertId()gives the new row's id;rowCount()counts affected rows.- Wrap connection logic in a
Databaseclass โ write once, require everywhere.