databases storage and caching
PDO
PDO, short for PHP Data Objects, is PHP's common database access layer. It lets PHP connect to databases such as SQLite, MySQL, MariaDB, and PostgreSQL through a consistent API.
PDO does not remove the need to understand SQL. It gives you a safer and more predictable way to connect, prepare statements, bind values, fetch rows, handle errors, and use transactions.
Creating A Connection
A PDO connection needs a DSN, a username, a password, and options. SQLite can run without a separate server, so it is ideal for examples.
<?php
declare(strict_types=1);
$pdo = new PDO('sqlite::memory:');
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
echo 'Connected' . PHP_EOL;
// Prints:
// Connected
For MySQL or PostgreSQL, the DSN changes, but the PDO methods are similar.
<?php
declare(strict_types=1);
$mysqlDsn = 'mysql:host=127.0.0.1;dbname=app;charset=utf8mb4';
$postgresDsn = 'pgsql:host=127.0.0.1;port=5432;dbname=app';
echo $mysqlDsn . PHP_EOL;
echo $postgresDsn . PHP_EOL;
// Prints:
// mysql:host=127.0.0.1;dbname=app;charset=utf8mb4
// pgsql:host=127.0.0.1;port=5432;dbname=app
Prepared Statements
Prepared statements separate the SQL command from the values. This is the normal way to protect against SQL injection.
<?php
declare(strict_types=1);
$pdo = new PDO('sqlite::memory:');
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
$pdo->exec('CREATE TABLE users (id INTEGER PRIMARY KEY, email TEXT NOT NULL UNIQUE)');
$insert = $pdo->prepare('INSERT INTO users (email) VALUES (:email)');
$insert->execute(['email' => 'sam@example.com']);
$select = $pdo->prepare('SELECT id, email FROM users WHERE email = :email');
$select->execute(['email' => 'sam@example.com']);
print_r($select->fetch());
// Prints:
// [id] => 1
// [email] => sam@example.com
Do not build SQL like "WHERE email = '$email'". The value may contain quotes, SQL syntax, or unexpected characters. Let PDO bind the value.
Fetching Rows
Common fetch methods include:
fetch()gets one row.fetchAll()gets all remaining rows.fetchColumn()gets one column from the next row.
<?php
declare(strict_types=1);
$pdo = new PDO('sqlite::memory:');
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
$pdo->exec('CREATE TABLE tasks (id INTEGER PRIMARY KEY, title TEXT NOT NULL, done INTEGER NOT NULL)');
$pdo->exec("INSERT INTO tasks (title, done) VALUES ('Write code', 1), ('Review code', 0)");
$count = $pdo->query('SELECT COUNT(*) FROM tasks')->fetchColumn();
echo 'tasks=' . $count . PHP_EOL;
// Prints:
// tasks=2
Be careful with fetchAll() on large result sets. It loads the entire result into memory.
Transactions
A transaction groups database changes so they succeed or fail together.
<?php
declare(strict_types=1);
$pdo = new PDO('sqlite::memory:');
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$pdo->exec('CREATE TABLE accounts (id INTEGER PRIMARY KEY, balance INTEGER NOT NULL)');
$pdo->exec('INSERT INTO accounts (id, balance) VALUES (1, 1000), (2, 500)');
$pdo->beginTransaction();
$pdo->prepare('UPDATE accounts SET balance = balance - :amount WHERE id = :id')->execute(['amount' => 200, 'id' => 1]);
$pdo->prepare('UPDATE accounts SET balance = balance + :amount WHERE id = :id')->execute(['amount' => 200, 'id' => 2]);
$pdo->commit();
foreach ($pdo->query('SELECT id, balance FROM accounts ORDER BY id') as $row) {
echo $row['id'] . ':' . $row['balance'] . PHP_EOL;
}
// Prints:
// 1:800
// 2:700
In real code, wrap the transaction in try/catch and roll back when an exception happens.
Error Mode
Set PDO::ATTR_ERRMODE to PDO::ERRMODE_EXCEPTION. Without exceptions, database errors can be easy to miss.
Also avoid displaying raw database errors to users. They may reveal table names, SQL fragments, or infrastructure details. Log safe diagnostic information and return a suitable application error.
What To Check
Before moving on, make sure you can:
- Create a PDO connection.
- Set exception mode and default fetch mode.
- Use prepared statements with named placeholders.
- Fetch one row, many rows, and one column.
- Use a transaction for related writes.
- Explain why PDO does not replace understanding SQL.
Practice
Practice: Use PDO Safely
Write a self-contained PHP script that uses PDO with SQLite in memory.
Requirements
- Create a PDO connection.
- Enable exceptions and associative fetch mode.
- Create a table.
- Insert rows with a prepared statement.
- Select one row with a prepared statement.
- Use
fetch()orfetchColumn()deliberately. - Include a transaction for two related updates.
Show solution
This solution uses SQLite so the full PDO example can run without a separate database service.
<?php
declare(strict_types=1);
$pdo = new PDO('sqlite::memory:');
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
$pdo->exec('CREATE TABLE users (id INTEGER PRIMARY KEY, email TEXT NOT NULL UNIQUE, credits INTEGER NOT NULL)');
$insert = $pdo->prepare('INSERT INTO users (email, credits) VALUES (:email, :credits)');
$insert->execute(['email' => 'sam@example.com', 'credits' => 10]);
$insert->execute(['email' => 'lee@example.com', 'credits' => 5]);
$select = $pdo->prepare('SELECT id, email, credits FROM users WHERE email = :email');
$select->execute(['email' => 'sam@example.com']);
$user = $select->fetch();
echo $user['email'] . ' has ' . $user['credits'] . ' credits' . PHP_EOL;
$pdo->beginTransaction();
$pdo->prepare('UPDATE users SET credits = credits - :amount WHERE email = :email')
->execute(['amount' => 2, 'email' => 'sam@example.com']);
$pdo->prepare('UPDATE users SET credits = credits + :amount WHERE email = :email')
->execute(['amount' => 2, 'email' => 'lee@example.com']);
$pdo->commit();
$totalCredits = $pdo->query('SELECT SUM(credits) FROM users')->fetchColumn();
echo 'total=' . $totalCredits . PHP_EOL;
// Prints:
// sam@example.com has 10 credits
// total=15
The inserts and selects use bound parameters. The two updates are in one transaction because a transfer should not half-complete.