Skip to content

PHP Tutorial PHP and MySQL

PDO Best Practices

Lesson 269 of 369 2 min read By Deepak Paswan Updated Oct 5, 2026

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

index.php
PHP
<?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.

index.php
PHP
<?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");
Output
OUTPUT
ORDER BY price DESC
ORDER BY name ASC

4. IN lists need one placeholder per value

index.php
PHP
<?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));
Output
OUTPUT
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

index.php
PHP
<?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.

Group related writes so that they succeed or fail together. See Handling Database Errors.

Columns used in WHERE, JOIN and ORDER BY often need an index. A search over a million rows without an index is slow.

query.sql
SQL
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.

index.php
PHP
<?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));
Output
OUTPUT
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.

info
Why this matters in Laravel
Laravel’s query builder and Eloquent follow these rules for you: they use prepared statements, bind values, and paginate. When you write plain PDO, you take over those responsibilities.