A prepared statement is an SQL query whose structure is sent separately from its values.
“SQL Injection” occurs when a hacker tricks your database into executing commands that you didn’t write.
The classic mistake (Concatenation):
// 🛑 DANGER OF DEATH
$sql = "SELECT * FROM usuarios WHERE email = '" . $_POST['email'] . "'";If the user types in the email field: ' OR '1'='1, the final query becomes SELECT * FROM usuarios WHERE email = '' OR '1'='1'. Since 1 always equals 1, the database returns all users, allowing the hacker to log in as admin without a password.
How do prepared statements solve this?
The logic changes radically. Instead of sending the query and data mixed together in a single phrase, we do it in two separate steps:
- Prepare: We send the SQL structure to the database without data. We say: “Hey, I’m going to want to execute this, but I’ll leave some empty slots (placeholders) to fill in later”. The database compiles and optimizes that command.
- Bind and execute: We send the values separately. The driver treats them as parameters and not as part of the SQL syntax, also respecting the specified type when we explicitly bind it.
Placeholders
Instead of putting the $email variable, we use a placeholder. In PDO there are two types, but we strongly recommend Named Placeholders (starting with a colon :) because they are easier to read.
// Define the template with placeholders (:email and :estado)
$sql = "SELECT * FROM usuarios WHERE email = :email AND activo = :estado";
// Prepare the statement
$stmt = $pdo->prepare($sql);Execution with array (the modern way)
The cleanest way to pass data is to deliver an associative array directly to the execute() method. The array keys must match the placeholders.
$emailUsuario = $_POST['email']; // Dirty/dangerous user data
// Execute by passing the data as a parameter; we still need to validate it
// according to the application's business rules.
$stmt->execute([
'email' => $emailUsuario,
'estado' => 1
]);
$resultado = $stmt->fetch();bindValue and bindParam methods (detailed level)
Sometimes you need more control (for example, specifying that a piece of data is an integer INT rather than text, for limits in LIMIT). For that, we bind the parameters before executing.
bindValue(':id', $id): Assigns the value at that exact moment. (The most commonly used).bindParam(':id', $id): Binds the variable. If you change the$idvariable after the bind but before the execute, the value changes.
$id = 5;
$limite = 10;
$stmt = $pdo->prepare("SELECT * FROM productos WHERE categoria_id = :id LIMIT :limite");
// For LIMIT, MySQL sometimes requires the data to be explicitly an integer
$stmt->bindValue(':id', $id, PDO::PARAM_INT);
$stmt->bindValue(':limite', $limite, PDO::PARAM_INT);
$stmt->execute();