Prepared Statement
debt(d5/e3/b3/t5)
Closest to 'specialist tool catches it' (d5) — semgrep/psalm/phpstan rules flag string-interpolated queries and missing prepare()/execute() patterns, but it's not a syntax error and default linters won't catch it.
Closest to 'simple parameterised fix' (e3) — quick_fix is replacing ->query("...{$var}") with ->prepare()/->execute([...]), a localised pattern swap per call site, slightly more than one line but mechanical.
Closest to 'localised tax' (b3) — applies to database access layer; once adopted as a convention it's lightweight, but every query site must follow the pattern across web/cli/queue contexts.
Closest to 'notable trap' (t5) — the misconception that PDO prepared statements are automatically safe (when ATTR_EMULATE_PREPARES is on by default) is a well-documented gotcha, plus the surprise that identifiers can't be bound.
Also Known As
TL;DR
Explanation
A prepared statement separates the SQL structure (sent to the database first) from the data values (bound and sent separately). The database parses the query once and treats the bound values as data, never as SQL syntax — making SQL injection structurally impossible regardless of what the user submits. Both PDO (with named :param or positional ? placeholders) and MySQLi (with bind_param()) support prepared statements. Never use string concatenation to build SQL queries.
Common Misconception
Why It Matters
Common Mistakes
- Binding only some parameters and concatenating the rest — every external value must be bound.
- Using PDO emulated prepares (the default) which can be bypassable — set PDO::ATTR_EMULATE_PREPARES to false.
- Confusing query parameters with identifiers — you cannot bind table or column names, only values.
- Reusing a statement handle but forgetting to call execute() again after changing bound values.
Code Examples
$id = $_GET['id'];
$rows = $pdo->query("SELECT * FROM users WHERE id = $id"); // SQLi
$stmt = $pdo->prepare("SELECT * FROM users WHERE id = ?");
$stmt->execute([$_GET['id']]);
$user = $stmt->fetch();