How do I prevent SQL injection in PHP?

Short answer

Use prepared statements with bound parameters. The query structure reaches the database before the values do, so a value can never be reinterpreted as SQL.

#Never build a query by concatenation

php
// CATASTROPHIC
$sql = "SELECT * FROM users WHERE email = '$email'";

Input of ' OR '1'='1 returns every row. Input of '; DROP TABLE users; -- does what it says.

#Do this instead

php
$pdo = new PDO('mysql:host=localhost;dbname=app;charset=utf8mb4', $user, $pass, [
    PDO::ATTR_ERRMODE            => PDO::ERRMODE_EXCEPTION,
    PDO::ATTR_EMULATE_PREPARES   => false,
    PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
]);

$stmt = $pdo->prepare('SELECT id, username FROM users WHERE email = ?');
$stmt->execute([$email]);
$user = $stmt->fetch();

ATTR_EMULATE_PREPARES => false matters — it makes PDO use the database's real prepared statements instead of doing string substitution in PHP.

#Named placeholders read better for several values

php
$stmt = $pdo->prepare(
    'INSERT INTO videos (title, track, views) VALUES (:title, :track, :views)'
);
$stmt->execute([
    'title' => $title,
    'track' => $track,
    'views' => $views,
]);

#Placeholders bind values, never identifiers

You cannot parameterise a table or column name. If a sort column comes from user input, check it against an allowlist:

php
$allowed = ['title', 'views', 'duration'];
$column = in_array($_GET['sort'] ?? '', $allowed, true) ? $_GET['sort'] : 'title';
$stmt = $pdo->query("SELECT * FROM videos ORDER BY $column");

The true third argument to in_array forces strict comparison — without it, PHP's loose typing can make surprising values match.

#What about IN clauses?

Build the placeholders to match the count:

php
$ids = [1, 2, 3];
$marks = implode(',', array_fill(0, count($ids), '?'));
$stmt = $pdo->prepare("SELECT * FROM videos WHERE id IN ($marks)");
$stmt->execute($ids);