Sentencias preparadas de PHP MySQL: Guía completa
Las sentencias preparadas aumentan la seguridad y eficiencia de las aplicaciones PHP que interactúan con bases de datos. Esta guía lo explica.
Las sentencias preparadas son la técnica más importante para escribir código PHP seguro que se comunica con una base de datos. Separan el comando SQL de los datos sobre los que opera, lo que detiene por completo la inyección SQL y además acelera las consultas que se ejecutan repetidamente. Esta guía explica qué son las sentencias preparadas, por qué son importantes y cómo usarlas con las extensiones MySQLi y PDO.
Esta página cubre:
- Qué es una sentencia preparada y por qué existe el modelo "compilar una vez, ejecutar muchas"
- Cómo escribir consultas
INSERTySELECTpreparadas con MySQLi - Los mismos patrones con PDO (marcadores de posición con nombre)
- Errores comunes: informes de errores, vinculación del tipo incorrecto y reutilización de sentencias
¿Qué son las sentencias preparadas?
Una sentencia preparada es una consulta SQL enviada a la base de datos en dos etapas:
- Preparar — se envía el SQL con
?(o:nombre) como marcadores de posición en lugar de valores reales. La base de datos analiza, compila y optimiza esta plantilla una sola vez. - Ejecutar — se envían los valores reales por separado. La base de datos los inserta en el plan ya compilado y lo ejecuta.
Dado que los valores viajan por un canal diferente al del texto SQL, la base de datos nunca confunde datos con comandos. Un valor como ' OR '1'='1 se trata como una cadena literal que buscar, no como SQL a ejecutar — exactamente por eso los ataques de inyección fallan contra las sentencias preparadas.
¿Por qué usar sentencias preparadas?
- Seguridad. La entrada del usuario nunca puede alterar la estructura de la consulta. Esta es la defensa recomendada contra la inyección SQL y la razón por la que nunca se deben construir consultas concatenando variables en una cadena.
- Rendimiento. La consulta se analiza y compila una sola vez. Si se ejecuta muchas veces (por ejemplo, insertar 1.000 filas en un bucle), la base de datos reutiliza el mismo plan en lugar de volver a analizarlo cada vez.
- Código más limpio. Los marcadores de posición eliminan la necesidad de escapar manualmente con
mysqli_real_escape_string()y de manejar comillas. Se vincula una variable y listo.
Regla general: en el momento en que cualquier parte de una consulta provenga de la entrada del usuario — un campo de formulario, un parámetro de URL, una cookie — usa una sentencia preparada.
Los pasos
Toda sentencia preparada sigue el mismo ciclo de vida:
- Conectarse a la base de datos.
- Preparar el SQL con marcadores de posición.
- Vincular las variables a los marcadores de posición.
- Ejecutar la sentencia.
- Obtener los resultados (para consultas
SELECT). - Cerrar la sentencia.
INSERT preparado con MySQLi
MySQLi usa marcadores de posición ? posicionales. Se vinculan con mysqli_stmt_bind_param(), donde el primer argumento es una cadena de tipos: s para string, i para integer, d para double/float, b para blob.
<?php
mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT); // throw on errors
$conn = mysqli_connect("localhost", "username", "password", "database");
$stmt = mysqli_prepare($conn, "INSERT INTO users (name, email) VALUES (?, ?)");
// "ss" => both placeholders are strings, in order
mysqli_stmt_bind_param($stmt, "ss", $name, $email);
$name = "John";
$email = "[email protected]";
mysqli_stmt_execute($stmt); // inserts John
$name = "Jane";
$email = "[email protected]";
mysqli_stmt_execute($stmt); // reuses the same compiled statement, inserts Jane
mysqli_stmt_close($stmt);
mysqli_close($conn);bind_param vincula por referencia, por lo que puedes cambiar $name/$email y llamar a execute() de nuevo sin volver a vincular — los nuevos valores se recogen automáticamente. Esa es la ventaja del "compilar una vez, ejecutar muchas" en acción.
SELECT preparado con MySQLi
Para un SELECT, se ejecuta la sentencia y luego se leen las filas. La forma más limpia es mysqli_stmt_get_result(), que devuelve un conjunto de resultados normal sobre el que se puede iterar:
<?php
mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT);
$conn = mysqli_connect("localhost", "username", "password", "database");
$stmt = mysqli_prepare($conn, "SELECT id, name FROM users WHERE email = ?");
mysqli_stmt_bind_param($stmt, "s", $email);
$email = "[email protected]";
mysqli_stmt_execute($stmt);
$result = mysqli_stmt_get_result($stmt);
while ($row = mysqli_fetch_assoc($result)) {
echo $row["id"] . ": " . $row["name"] . "\n";
}
mysqli_stmt_close($stmt);
mysqli_close($conn);Sentencias preparadas con PDO
PDO es la otra extensión de base de datos común y muchos desarrolladores la prefieren porque funciona con diferentes sistemas de bases de datos y admite marcadores de posición con nombre (:email), que son más fáciles de leer.
<?php
$pdo = new PDO(
"mysql:host=localhost;dbname=database;charset=utf8mb4",
"username",
"password",
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]
);
// INSERT with named placeholders
$stmt = $pdo->prepare("INSERT INTO users (name, email) VALUES (:name, :email)");
$stmt->execute([":name" => "John", ":email" => "[email protected]"]);
// SELECT and fetch
$stmt = $pdo->prepare("SELECT id, name FROM users WHERE email = :email");
$stmt->execute([":email" => "[email protected]"]);
foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $row) {
echo $row["id"] . ": " . $row["name"] . "\n";
}Nótese que no se declaran tipos con PDO — se pasa un array asociativo de valores directamente a execute(). Configurar PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION hace que PDO lance excepciones en caso de fallo, por lo que los problemas nunca se ignoran silenciosamente.
Errores comunes
- Olvidar el informe de errores. Por defecto, MySQLi puede fallar silenciosamente. Llama a
mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT)(o usa las excepciones de PDO) para que una consulta incorrecta lance una excepción en lugar de devolverfalse. - Vincular el número incorrecto de tipos. La cadena de tipos en
bind_paramdebe tener exactamente un carácter por cada?."ss"para dos marcadores,"si"para una cadena y luego un entero. - Colocar un marcador donde SQL no lo permite. Solo se pueden vincular valores, no identificadores.
WHERE id = ?funciona;ORDER BY ?oSELECT * FROM ?no — los nombres de tablas y columnas deben estar en el código o en una lista blanca. - Concatenar "solo este" valor. No hay excepción segura. Si vino de un usuario, vincúlalo.
Conclusión
Las sentencias preparadas dividen una consulta en una plantilla SQL compilada más los valores que la completan. Esa separación es lo que las hace tanto seguras (a prueba de inyección) como rápidas (analizadas una vez, ejecutadas muchas veces). Usa los marcadores de posición ? de MySQLi o los marcadores con nombre :valor de PDO, pero siempre vincula la entrada del usuario en lugar de construir SQL a mano.
Continúa con los capítulos relacionados: Conectar a MySQL, Insertar datos, Seleccionar datos, y la referencia de mysqli_prepare().