A short checklist for writing database code you can trust.
1. Prepared statements for every outside value
No exceptions. No "... WHERE id = $id". See Prepared Statements.
2. Safe connection options
<?php
$options = [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false,
];
And charset=utf8mb4 in the DSN.
3. Allow-lists for names and keywords
Placeholders cannot stand for column names, table names or ASC/DESC. Check them against a fixed list.
<?php
function orderBy(string $column, string $direction): string
{
$columns = ["name", "price", "stock"];
$directions = ["ASC", "DESC"];
$column = in_array($column, $columns, true) ? $column : "name";
$direction = in_array(strtoupper($direction), $directions, true) ? strtoupper($direction) : "ASC";
return "ORDER BY $column $direction";
}
echo orderBy("price", "desc") . "\n";
echo orderBy("1; DROP TABLE x", "sideways");
ORDER BY price DESC
ORDER BY name ASC
4. IN lists need one placeholder per value
<?php
$pdo = require __DIR__ . "/db.php";
$ids = [1, 3, 5];
$placeholders = implode(",", array_fill(0, count($ids), "?"));
$statement = $pdo->prepare("SELECT name FROM products WHERE id IN ($placeholders) ORDER BY id");
$statement->execute($ids);
echo implode(", ", $statement->fetchAll(PDO::FETCH_COLUMN));
Notebook, Bag, Diary
The SQL string contains only ? characters that you generated yourself. The values still go through execute().
5. Select only the columns you need
Name them. It is faster, and it never leaks a column that was added later (password_hash).
6. Do not query inside loops
<?php
// Slow: one query per post (the "N+1" problem)
foreach ($posts as $post) {
$author = $pdo->query("SELECT name FROM users WHERE id = " . (int) $post["user_id"])->fetch();
}
// Better: one query with a JOIN
$posts = $pdo->query("SELECT p.title, u.name AS author FROM posts p JOIN users u ON u.id = p.user_id")->fetchAll();
7. Paginate big lists
Never load an entire large table. Use LIMIT and OFFSET. See Pagination Basics.
8. Use transactions for related changes
Group related writes so that they succeed or fail together. See Handling Database Errors.
9. Index what you search
Columns used in WHERE, JOIN and ORDER BY often need an index. A search over a million rows without an index is slow.
CREATE INDEX idx_products_category ON products (category);
10. Least privilege
The database user for your app should have only the rights it needs (SELECT, INSERT, UPDATE, DELETE on one database). Not root, not DROP, not access to other databases. Run migrations with a separate, more powerful user.
11. Keep SQL out of templates
Put queries in functions or small classes (repositories), not in the middle of HTML. That makes them reusable and testable.
<?php
final class ProductRepository
{
public function __construct(private PDO $pdo) {}
public function find(int $id): ?array
{
$statement = $this->pdo->prepare("SELECT id, name, stock FROM products WHERE id = :id");
$statement->execute(["id" => $id]);
return $statement->fetch() ?: null;
}
}
$repo = new ProductRepository(require __DIR__ . "/db.php");
echo $repo->find(2)["name"] . " / ";
var_dump($repo->find(999));
Pen / NULL
12. Store passwords as hashes, never plain
See Password Hashing.
13. Back up
A database without backups is a risk. Know how to export and restore (mysqldump, phpMyAdmin export, or your host’s backup tools), and test a restore.
14. Keep credentials safe
Use environment variables or a config file outside the web root. Never commit passwords to Git.