Consultas preparadas con mysqli
Las consultas preparadas (prepared statements) son la solución definitiva contra la inyección SQL. En lugar de construir la consulta concatenando valores del usuario, se define primero la estructura con marcadores de posición (?) y se vinculan los valores por separado. MySQL los trata siempre como datos, nunca como código SQL.
Por qué son necesarias
Este código es vulnerable a SQL injection:
<?php
// NUNCA hagas esto con datos del usuario
$email = $_POST["email"]; // ¿Y si el usuario escribe: ' OR '1'='1 ?
mysqli_query($link, "SELECT * FROM usuarios WHERE email = '$email'");
Con prepared statements, el valor siempre se trata como dato literal, sin importar lo que contenga.
SELECT con prepared statement
El flujo es: mysqli_prepare() → mysqli_stmt_bind_param() → mysqli_stmt_execute() → mysqli_stmt_get_result():
<?php
$stmt = mysqli_prepare($link, "SELECT id, nombre, email FROM usuarios WHERE email = ?");
if (!$stmt) {
die("Error preparando consulta: " . mysqli_error($link));
}
$email = $_POST["email"];
mysqli_stmt_bind_param($stmt, "s", $email); // "s" = string
mysqli_stmt_execute($stmt);
$resultado = mysqli_stmt_get_result($stmt);
$usuario = mysqli_fetch_assoc($resultado);
if ($usuario === null) {
echo "Usuario no encontrado.";
} else {
echo "Bienvenido, " . htmlspecialchars($usuario["nombre"]);
}
mysqli_stmt_close($stmt);
Tipos en mysqli_stmt_bind_param()
El segundo argumento es una cadena de tipos, uno por cada ? en la consulta:
s: stringi: integerd: double (decimal)b: blob (datos binarios)
<?php
// Dos parámetros: un string y un entero
$stmt = mysqli_prepare($link, "SELECT * FROM pedidos WHERE email = ? AND estado_id = ?");
mysqli_stmt_bind_param($stmt, "si", $email, $estado_id);
INSERT con prepared statement
<?php
$stmt = mysqli_prepare($link, "INSERT INTO usuarios (nombre, email, hash_clave) VALUES (?, ?, ?)");
mysqli_stmt_bind_param($stmt, "sss", $nombre, $email, $hash);
$nombre = filter_input(INPUT_POST, "nombre", FILTER_SANITIZE_FULL_SPECIAL_CHARS);
$email = filter_input(INPUT_POST, "email", FILTER_VALIDATE_EMAIL);
$hash = password_hash($_POST["contrasena"], PASSWORD_DEFAULT);
if (!mysqli_stmt_execute($stmt)) {
error_log("Error al insertar: " . mysqli_stmt_error($stmt));
echo "No se pudo crear el usuario.";
} else {
echo "Usuario creado con ID: " . mysqli_insert_id($link);
}
mysqli_stmt_close($stmt);
UPDATE y DELETE
<?php
// UPDATE
$stmt = mysqli_prepare($link, "UPDATE usuarios SET nombre = ? WHERE id = ?");
mysqli_stmt_bind_param($stmt, "si", $nombre_nuevo, $usuario_id);
mysqli_stmt_execute($stmt);
echo "Filas actualizadas: " . mysqli_stmt_affected_rows($stmt);
mysqli_stmt_close($stmt);
// DELETE
$stmt = mysqli_prepare($link, "DELETE FROM sesiones WHERE usuario_id = ? AND expira < NOW()");
mysqli_stmt_bind_param($stmt, "i", $usuario_id);
mysqli_stmt_execute($stmt);
mysqli_stmt_close($stmt);
Transacciones
Una transacción agrupa varias operaciones en una unidad atómica: o se ejecutan todas o no se ejecuta ninguna. Imprescindible cuando varias escrituras deben ser consistentes entre sí (por ejemplo, transferencia entre cuentas: restar de una y sumar en otra).
<?php
mysqli_autocommit($link, false); // desactiva el commit automático
$ok = true;
$stmt1 = mysqli_prepare($link, "UPDATE cuentas SET saldo = saldo - ? WHERE id = ?");
mysqli_stmt_bind_param($stmt1, "di", $importe, $cuenta_origen);
if (!mysqli_stmt_execute($stmt1)) { $ok = false; }
mysqli_stmt_close($stmt1);
$stmt2 = mysqli_prepare($link, "UPDATE cuentas SET saldo = saldo + ? WHERE id = ?");
mysqli_stmt_bind_param($stmt2, "di", $importe, $cuenta_destino);
if (!mysqli_stmt_execute($stmt2)) { $ok = false; }
mysqli_stmt_close($stmt2);
if ($ok) {
mysqli_commit($link);
echo "Transferencia completada.";
} else {
mysqli_rollback($link);
error_log("Transacción fallida: " . mysqli_error($link));
echo "Error al procesar la transferencia.";
}
mysqli_autocommit($link, true); // restaura el modo normal
Nota: Por defecto mysqli está en modo autocommit: cada consulta se confirma inmediatamente al ejecutarse. mysqli_autocommit($link, false) lo desactiva hasta que llames a mysqli_commit() o mysqli_rollback().
Recapitulación
- Los prepared statements previenen SQL injection: los valores del usuario se tratan siempre como datos, nunca como código.
- Flujo:
mysqli_prepare()→mysqli_stmt_bind_param(tipos, vars...)→mysqli_stmt_execute(). Para SELECT añademysqli_stmt_get_result(). - Tipos en
bind_param:s(string),i(int),d(double),b(blob). - Cierra los statements con
mysqli_stmt_close()cuando termines. - Las transacciones (
mysqli_autocommit(false)→ operaciones →mysqli_commit()omysqli_rollback()) garantizan que un conjunto de operaciones se ejecute de forma atómica.
En la próxima lección: estructura de un proyecto PHP: organización de carpetas, separación de responsabilidades y seguridad básica.