🔌 PHP and MySQL Integration
A database is only useful once your code can talk to it. PHP has two modern, battle-tested ways to reach MySQL — MySQLi and PDO. This lesson shows you how to open a connection, run queries, read results, perform full CRUD, and structure connection code so it stays secure and maintainable.
🎯 Learning Objectives
By the end of this lesson, you will be able to:
- Compare the MySQLi and PDO extensions and choose the right one
- Connect to MySQL from PHP using both APIs
- Run queries and fetch results as associative arrays, objects, or full result sets
- Perform CRUD (Create, Read, Update, Delete) operations from PHP
- Centralize connection code and load credentials from configuration or environment variables
Estimated Time: 45–60 minutes • Difficulty: Intermediate
Hands-on: Build a reusable PDO connection class and drive a small user table through all four CRUD operations.
In This Lesson
How PHP Talks to MySQL
PHP is the scripting layer that generates dynamic pages; MySQL is where the data lives. On every request, your PHP code opens a connection to the MySQL server, sends one or more SQL queries, receives a result set, and turns those rows into HTML, JSON, or whatever the page needs.
💡 The restaurant analogy. MySQL is the kitchen and pantry, where all the ingredients (data) are stored and prepared. PHP is the waitstaff: it takes the customer's request, carries the "order" (an SQL query) to the kitchen, and brings the finished plate (the result set) back to the table. The database connection is the service door between dining room and kitchen, and the connection credentials are the staff badge that unlocks it.
⚠️ One extension is gone for good
The ancient mysql_* functions (mysql_connect, mysql_query…) were deprecated in PHP 5.5 and removed entirely in PHP 7.0. If you find them in a tutorial, close the tab. Modern PHP uses only MySQLi or PDO.
MySQLi vs PDO
Both are safe, modern, and support prepared statements. The choice usually comes down to whether you might switch databases later.
| Feature | MySQLi | PDO |
|---|---|---|
| Databases supported | MySQL / MariaDB only | 12+ (MySQL, PostgreSQL, SQLite, …) |
| API style | Procedural and object-oriented | Object-oriented only |
| Prepared statements | Yes (positional ?) | Yes (positional ? and named :param) |
| Error handling | Errors or exceptions | Exceptions |
| Best for | MySQL-only projects | Cross-database / portable code |
✅ Which should you pick?
For most new projects, prefer PDO: named parameters make queries readable, its exception-based errors are cleaner, and you can move to PostgreSQL or SQLite without rewriting your data layer. Reach for MySQLi only when you need a MySQL-specific feature or you're maintaining code that already uses it.
Confirm the extensions are loaded
<?php
// A quick self-check you can drop into any script
echo extension_loaded('mysqli') ? "MySQLi ready\n" : "MySQLi missing\n";
echo extension_loaded('pdo_mysql') ? "PDO MySQL ready\n" : "PDO MySQL missing\n";
On Ubuntu, install both with sudo apt install php-mysql then restart your web server. On Windows stacks (XAMPP/WAMP) uncomment extension=mysqli and extension=pdo_mysql in php.ini.
Opening a Connection
PDO (recommended)
<?php
$host = 'localhost';
$db = 'myapp_db';
$user = 'webuser';
$pass = 'ChangeMe_Str0ng!';
$dsn = "mysql:host=$host;dbname=$db;charset=utf8mb4";
// Sensible defaults for every connection
$options = [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, // throw on error
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, // associative rows
PDO::ATTR_EMULATE_PREPARES => false, // use real prepared statements
];
try {
$pdo = new PDO($dsn, $user, $pass, $options);
echo "Connected with PDO";
} catch (PDOException $e) {
// Log the detail, show the user nothing sensitive
error_log($e->getMessage());
exit('Database connection failed.');
}
MySQLi (object-oriented)
<?php
// Make MySQLi throw exceptions instead of warnings (default since PHP 8.1)
mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT);
try {
$mysqli = new mysqli('localhost', 'webuser', 'ChangeMe_Str0ng!', 'myapp_db');
$mysqli->set_charset('utf8mb4');
echo "Connected with MySQLi";
} catch (mysqli_sql_exception $e) {
error_log($e->getMessage());
exit('Database connection failed.');
}
📖 Key terms
DSN (Data Source Name): the connection string PDO uses to describe the driver, host, database, and charset.
Connection object: the live handle ($pdo or $mysqli) you use for every subsequent query; PHP closes it automatically when the script ends.
Running Queries & Fetching Results
For a query with no user input, you can use the simple query() method. (The moment any value comes from a user, switch to prepared statements — the whole focus of the next lesson.)
PDO — fetch row by row
<?php
$stmt = $pdo->query('SELECT id, username, email FROM users LIMIT 5');
foreach ($stmt as $row) { // PDOStatement is iterable
echo "{$row['username']} — {$row['email']}\n";
}
PDO — fetch everything at once
<?php
$users = $pdo->query('SELECT id, username FROM users')->fetchAll();
foreach ($users as $user) {
echo $user['username'] . "\n";
}
MySQLi — object-oriented fetch
<?php
$result = $mysqli->query('SELECT id, username, email FROM users LIMIT 5');
if ($result->num_rows > 0) {
while ($row = $result->fetch_assoc()) {
echo "{$row['username']} — {$row['email']}\n";
}
}
$result->free(); // release the result set
💡 Fetch modes at a glance
PDO::FETCH_ASSOC— column-name keyed array:$row['username']PDO::FETCH_OBJ— a stdClass object:$row->usernamePDO::FETCH_NUM— numerically indexed array:$row[1]fetchAll()— returns every row in one array (great for small result sets)
Insert, Update & Delete
The four data operations — Create, Read, Update, Delete — are the backbone of nearly every application. Here they are with PDO prepared statements, which safely bind user data into the query.
<?php
// CREATE — insert and get the new id
$sql = 'INSERT INTO users (username, email, password_hash, created_at)
VALUES (:username, :email, :password, NOW())';
$stmt = $pdo->prepare($sql);
$stmt->execute([
':username' => 'johndoe',
':email' => 'john@example.com',
':password' => password_hash('secret123', PASSWORD_DEFAULT),
]);
$newId = $pdo->lastInsertId();
echo "Created user #$newId\n";
// READ — one record
$stmt = $pdo->prepare('SELECT * FROM users WHERE id = :id');
$stmt->execute([':id' => $newId]);
$user = $stmt->fetch();
echo "{$user['username']} ({$user['email']})\n";
// UPDATE — rowCount() reports how many rows changed
$stmt = $pdo->prepare('UPDATE users SET email = :email WHERE id = :id');
$stmt->execute([':email' => 'john.doe@example.com', ':id' => $newId]);
echo "Updated {$stmt->rowCount()} row(s)\n";
// DELETE
$stmt = $pdo->prepare('DELETE FROM users WHERE id = :id');
$stmt->execute([':id' => $newId]);
echo "Deleted {$stmt->rowCount()} row(s)\n";
Typical output
Created user #42
johndoe (john@example.com)
Updated 1 row(s)
Deleted 1 row(s)
⚠️ Hash passwords, never store them plain
Notice password_hash($password, PASSWORD_DEFAULT) above. PHP's built-in hashing uses bcrypt by default and salts automatically. Verify on login with password_verify($input, $storedHash). We cover this in depth in the next lesson.
Centralizing & Securing Connections
Copy-pasting connection code into every file is a maintenance and security nightmare. Instead, wrap it in one class and load credentials from outside your source code.
A reusable database class
<?php
// Database.php
class Database
{
private ?PDO $conn = null;
public function connect(): PDO
{
if ($this->conn !== null) {
return $this->conn; // reuse an open connection
}
// Credentials come from the environment, not the code
$host = getenv('DB_HOST') ?: 'localhost';
$name = getenv('DB_NAME');
$user = getenv('DB_USER');
$pass = getenv('DB_PASS');
$dsn = "mysql:host=$host;dbname=$name;charset=utf8mb4";
$this->conn = new PDO($dsn, $user, $pass, [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false,
]);
return $this->conn;
}
}
Using it
<?php
require 'Database.php';
$pdo = (new Database())->connect();
$stmt = $pdo->query('SELECT username FROM users LIMIT 5');
foreach ($stmt as $row) {
echo $row['username'] . "\n";
}
Keep secrets out of the repo
Store credentials in a .env file that is listed in .gitignore, and load it with the popular vlucas/phpdotenv package:
# .env (NEVER commit this file)
DB_HOST=localhost
DB_NAME=myapp_db
DB_USER=webuser
DB_PASS=ChangeMe_Str0ng!
<?php
require 'vendor/autoload.php';
$dotenv = Dotenv\Dotenv::createImmutable(__DIR__);
$dotenv->load(); // now getenv('DB_PASS') and $_ENV['DB_PASS'] work
✅ Why this matters
Environment variables let the same code run in development, staging, and production with different credentials — and they keep passwords out of version control, where leaked secrets are one of the most common breaches.
Handling Errors
With PDO::ERRMODE_EXCEPTION (or MySQLi's report mode) set, any database failure throws an exception you can catch. The golden rule: log the detail, show the user nothing sensitive.
<?php
try {
$stmt = $pdo->prepare('SELECT * FROM users WHERE id = :id');
$stmt->execute([':id' => $id]);
$user = $stmt->fetch();
if (!$user) {
echo 'User not found.';
}
} catch (PDOException $e) {
// Full detail goes to the server log
error_log('DB error: ' . $e->getMessage() . ' in ' . $e->getFile() . ':' . $e->getLine());
// The user gets a friendly, generic message
http_response_code(500);
echo 'Sorry, a system error occurred. Please try again later.';
}
⚠️ Never leak raw SQL errors in production
A message like "SQLSTATE... near WHERE username =" hands attackers a map of your schema. Show detailed errors only in a development environment, gated behind a check like if (getenv('APP_ENV') === 'development').
Hands-on Exercise
🏋️ A PDO-powered mini user manager
Objective: Wire PHP to MySQL and exercise all four CRUD operations through a centralized connection.
Instructions:
- Create a
Databaseclass that returns a configured PDO connection using credentials from environment variables. - Write a script that inserts two users (hash their passwords), reads them all back, updates one email, and deletes one user.
- After each write, print how many rows were affected using
rowCount()andlastInsertId(). - Wrap everything in a
try / catchthat logs the real error and shows a generic message.
💡 Hint
Insert with named placeholders (:username, :email, :password) and pass them as an associative array to execute(). For the "read all" step, fetchAll() returns every row so you can loop with a simple foreach.
✅ Sample solution
<?php
require 'Database.php'; // the class from the lesson
try {
$pdo = (new Database())->connect();
// CREATE
$insert = $pdo->prepare(
'INSERT INTO users (username, email, password_hash, created_at)
VALUES (:u, :e, :p, NOW())'
);
foreach ([['ada','ada@ex.com'], ['grace','grace@ex.com']] as [$u, $e]) {
$insert->execute([
':u' => $u,
':e' => $e,
':p' => password_hash('secret123', PASSWORD_DEFAULT),
]);
echo "Inserted #{$pdo->lastInsertId()}\n";
}
// READ
foreach ($pdo->query('SELECT id, username, email FROM users') as $row) {
echo "{$row['id']}: {$row['username']} ({$row['email']})\n";
}
// UPDATE
$upd = $pdo->prepare('UPDATE users SET email = :e WHERE username = :u');
$upd->execute([':e' => 'ada.lovelace@ex.com', ':u' => 'ada']);
echo "Updated {$upd->rowCount()} row(s)\n";
// DELETE
$del = $pdo->prepare('DELETE FROM users WHERE username = :u');
$del->execute([':u' => 'grace']);
echo "Deleted {$del->rowCount()} row(s)\n";
} catch (PDOException $e) {
error_log($e->getMessage());
echo 'A database error occurred.';
}
🎯 Quick Quiz
Question 1: You want your data-access code to work with MySQL now but possibly PostgreSQL later. Which API fits best?
Question 2: After a successful INSERT with PDO, how do you get the auto-increment id of the new row?
Question 3: When a database error occurs in production, what should the end user see?
Summary & Quiz
🎉 Key Takeaways
- PHP reaches MySQL through two modern extensions: MySQLi (MySQL-only) and PDO (many databases). The old
mysql_*functions are gone. - Prefer PDO for new code: named parameters, cleaner exceptions, and portability.
- Set
utf8mb4andERRMODE_EXCEPTIONon every connection. - Fetch results as associative arrays, objects, or all at once with
fetchAll(). - Centralize connection code in a class and load credentials from environment variables, never hardcoded.
- On errors: log the detail, show the user a generic message.
📚 Further Reading
🚀 What's Next?
You can now connect and run CRUD from PHP. But the moment user input touches your queries, you face the web's most infamous vulnerability. Next, Prepared Statements and Security shows you how to defeat SQL injection and build a hardened data layer.
🎉 Great progress!
Your PHP code and MySQL database are now talking. Time to make that conversation bulletproof.