PDO Prepared Statements
PDO Prepared Statements Why Prepared Statements? In simple terms: A prepared statement keeps the SQL command and the data separate. You write the query with placeholders, and PDO sends the values separately (or escapes them safely). Whatever a user types — quotes, SQL keywords, special characters — is treated as plain data and can never change the query. This is the essential defense against SQL injection.
Technically: PDO::prepare() returns a PDOStatement for an SQL template with positional (?) or named (:name) placeholders. Values are supplied to execute() as an array, or bound beforehand with bindValue() / bindParam(), optionally with an explicit type (PDO::PARAM_INT, PDO::PARAM_STR, PDO::PARAM_BOOL, PDO::PARAM_NULL). A prepared statement can be executed many times with different values.
The examples use the pdo() helper from PDO Connect (exceptions enabled, FETCH_ASSOC default, native prepares).
Named Placeholders <?php require DIR . '/src/pdo.php';
$stmt = pdo()->prepare( 'SELECT name, price FROM products WHERE category = :category AND price <= :max_price ORDER BY price' );
$stmt->execute([ 'category' => 'electronics', 'max_price' => 1000, ]);
foreach ($stmt as $row) { echo "{$row['name']}: ₹{$row['price']}\n"; } Output Wireless Mouse: ₹599.00 How It Works :category and :max_price are named placeholders. Their names document what each value means — helpful in queries with many parameters. execute() receives an associative array whose keys match the placeholder names (a leading colon in the keys is optional). A PDOStatement can be iterated directly with foreach, fetching one row per iteration in the default fetch mode. The Desk Lamp (₹1,299) is excluded by the price limit. Positional Placeholders <?php require DIR . '/src/pdo.php';
$stmt = pdo()->prepare('SELECT name FROM products WHERE sku = ?');
foreach (['NB-A5', 'NOPE-99'] as $sku) { $stmt->execute([$sku]); $name = $stmt->fetchColumn();
echo $sku . ': ' . ($name === false ? 'not found' : $name) . "\n"; } Output NB-A5: Notebook A5 NOPE-99: not found ? placeholders are filled in order from a list array. fetchColumn() returns the first column of the next row, or false when there is no row. The statement is prepared once and executed twice. Positional and named placeholders cannot be mixed in one statement. Named placeholders are generally clearer; positional ones are convenient for short queries and generated lists.
Insert, Update, and Delete <?php require DIR . '/src/pdo.php';
$pdo = pdo();
$insert = $pdo->prepare('INSERT INTO customers (name, email, city) VALUES (:name, :email, :city)'); $insert->execute(['name' => 'Nila', 'email' => 'nila@example.com', 'city' => 'Madurai']);
$id = $pdo->lastInsertId(); echo "New customer ID: $id (" . get_debug_type($id) . ")\n";
$restock = $pdo->prepare('UPDATE products SET stock = stock + :quantity WHERE sku = :sku'); $restock->execute(['quantity' => 5, 'sku' => 'MOUSE-01']); echo "Rows updated: " . $restock->rowCount() . "\n";
$restock->execute(['quantity' => 5, 'sku' => 'NOPE-99']); echo "Rows updated: " . $restock->rowCount() . "\n"; Output (the ID depends on your data) New customer ID: 15 (string) Rows updated: 1 Rows updated: 0 lastInsertId() returns the generated ID as a string; cast it with (int) when an integer is needed. rowCount() returns the number of rows affected by the last INSERT, UPDATE, or DELETE executed with this statement. For MySQL, an UPDATE that does not actually change a row is not counted. Do not rely on rowCount() for SELECT statements; its behavior varies between databases. Count rows with COUNT(*) or count($rows) instead. Binding Values With Types When a parameter needs a specific type, bind it explicitly. The most important case is LIMIT and OFFSET, which must be integers:
<?php require DIR . '/src/pdo.php';
$stmt = pdo()->prepare('SELECT name, price FROM products ORDER BY price DESC LIMIT :limit'); $stmt->bindValue(':limit', 2, PDO::PARAM_INT); $stmt->execute();
foreach ($stmt->fetchAll() as $row) { echo "{$row['name']}: ₹{$row['price']}\n"; } Output Desk Lamp: ₹1299.00 Backpack: ₹899.00 Values passed in the execute() array are always treated as strings. With emulated prepares, LIMIT :limit would then become LIMIT '2', which is a syntax error. bindValue() with PDO::PARAM_INT sends a real integer and works in both modes.
Type Constant Use For PDO::PARAM_STR Strings (default), dates, decimals PDO::PARAM_INT Integers, especially LIMIT/OFFSET PDO::PARAM_BOOL Booleans PDO::PARAM_NULL SQL NULL bindValue() vs bindParam() Method Binds Value Used bindValue(':p', $value) A value The value at the time of binding bindParam(':p', $variable) A variable, by reference The variable's value at the time of execute() <?php require DIR . '/src/pdo.php';
$stmt = pdo()->prepare('SELECT stock FROM products WHERE sku = :sku'); $stmt->bindParam(':sku', $sku);
foreach (['PEN-01', 'LAMP-01'] as $sku) { $stmt->execute(); echo "$sku: " . $stmt->fetchColumn() . " in stock\n"; } Output PEN-01: 120 in stock LAMP-01: 6 in stock bindParam() binds the variable once; each execute() uses its current value. In modern code, passing values to execute([...]) is usually simpler and clearer.
Lists of Values: IN (...) <?php require DIR . '/src/pdo.php';
$skus = ['PEN-01', 'BAG-02', 'NOPE-99'];
$placeholders = implode(', ', array_fill(0, count($skus), '?')); $stmt = pdo()->prepare("SELECT sku, name FROM products WHERE sku IN ($placeholders) ORDER BY id"); $stmt->execute($skus);
print_r($stmt->fetchAll(PDO::FETCH_KEY_PAIR)); Output Array ( [PEN-01] => Gel Pen [BAG-02] => Backpack ) One placeholder is generated per value; the values are passed in the same order. (FETCH_KEY_PAIR is explained in PDO Fetch Modes.) Check for an empty list first — IN () is invalid SQL.
Identifiers Are Not Parameters Placeholders can only stand for values, not for table names, column names, ASC/DESC, or other SQL syntax. When such parts depend on input — a selectable sort column, for example — map the input to an allow-list of SQL fragments written in your code (see MySQL Order By).
Common Mistakes Concatenating Values Into SQL Effect: SQL injection and quoting errors.
Fix: Use placeholders for every value.
Quoting Placeholders Effect: WHERE sku = ':sku' compares with the literal text :sku.
Fix: Write placeholders without quotes.
Missing or Extra Parameters Effect: PDOException with SQLSTATE HY093 ("Invalid parameter number").
Fix: Supply exactly one value per placeholder, with matching names.
Passing LIMIT Values as Strings Effect: SQL syntax errors with emulated prepares.
Fix: Bind with PDO::PARAM_INT.
Mixing ? and :name Effect: Error.
Fix: Use one placeholder style per statement.
Best Practices Prepare every statement that contains values not written in your code. Prefer named placeholders for readability; use positional ones for generated lists. Pass values to execute(); use bindValue() with types where the type matters. Reuse prepared statements for repeated operations. Check rowCount() after modifications and lastInsertId() after inserts. Official reference: https://www.php.net/manual/en/pdo.prepared-statements.php