PDO (PHP Data Objects) es la forma nativa de hablar con una base de datos. Conecta con new PDO($dsn, $user, $password) y ejecuta cualquier consulta que contenga un valor con prepare() y execute(), para que el valor se envíe separado del SQL:
<?php
$pdo = new PDO('mysql:host=localhost;dbname=shop;charset=utf8mb4', 'shop_user', 'secret', [
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
]);
$stmt = $pdo->prepare('SELECT id, name FROM users WHERE email = ?');
$stmt->execute([$email]);
$user = $stmt->fetch(); // an array, or false if no row matched
Los ejemplos de esta página necesitan un servidor de base de datos, así que se muestran como código simple. El ejemplo de inyección SQL de más abajo sí se ejecuta.
Conectar con MySQL mediante PDO
El primer argumento es un DSN: el nombre del driver y luego los datos de conexión. Pon la conexión en un solo lugar e inclúyela donde la necesites.
<?php
// db.php
$dsn = 'mysql:host=127.0.0.1;port=3306;dbname=shop;charset=utf8mb4';
$options = [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, // the default since PHP 8.0
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, // rows as ['id' => 1, ...]
PDO::ATTR_EMULATE_PREPARES => false, // real prepared statements, real int types
];
try {
$pdo = new PDO($dsn, getenv('DB_USER'), getenv('DB_PASS'), $options);
} catch (PDOException $e) {
error_log($e->getMessage());
http_response_code(500);
exit('Database unavailable.');
}
return $pdo;
charset=utf8mb4va en el DSN. Sin él, las tildes y los emojis pueden llegar como?o como bytes corruptos.- Nunca imprimas
$e->getMessage()a los visitantes: puede contener el host y el nombre de usuario. Regístralo en un log. - Mantén la contraseña fuera del código, en una variable de entorno o en un archivo de configuración fuera de la raíz web.
Las demás bases de datos solo cambian el DSN: pgsql:host=localhost;dbname=shop para PostgreSQL y sqlite:/var/data/app.db para un archivo SQLite. Cada una necesita instalado su driver de PDO (pdo_mysql, pdo_pgsql, pdo_sqlite); php -m lista los que tienes.
Sentencias preparadas con ? y :name
Un marcador indica dónde va un valor. Usa marcadores posicionales ? con una lista, o marcadores con nombre con un array asociativo:
<?php
// Positional
$stmt = $pdo->prepare('SELECT * FROM products WHERE category = ? AND price < ?');
$stmt->execute(['books', 20]);
// Named: easier to read when there are many values
$stmt = $pdo->prepare('SELECT * FROM products WHERE category = :cat AND price < :max');
$stmt->execute(['cat' => 'books', 'max' => 20]);
// bindValue when you want to set the type explicitly
$stmt = $pdo->prepare('SELECT * FROM products ORDER BY id LIMIT :limit');
$stmt->bindValue('limit', 10, PDO::PARAM_INT);
$stmt->execute();
No pongas comillas alrededor de un marcador: WHERE name = '?' compara con el texto literal ?. Una sentencia preparada también se puede ejecutar muchas veces con valores distintos, así que prepárala una vez antes del bucle, no dentro de él.
Por qué es peligroso concatenar cadenas en SQL
Este es el único bloque de la página que se ejecuta: construye la consulta de la forma incorrecta y de la correcta con la misma entrada. Cambia $email y vuelve a ejecutarlo.
En la consulta concatenada, la comilla de la entrada cierra la cadena, y OR '1'='1' hace que la condición sea verdadera para todas las filas: la consulta devuelve todos los usuarios. En un formulario de inicio de sesión eso significa entrar sin conocer la contraseña, y si el driver acepta varias sentencias en una llamada, una entrada como '; DROP TABLE users; -- ejecuta una segunda sentencia elegida por el atacante. En la versión preparada la base de datos recibe el SQL y el valor por separado, así que la comilla es solo un carácter dentro de un email que no coincide con nadie.
Las funciones de escape como addslashes() no son una solución: se les escapan casos que dependen del juego de caracteres de la conexión. Usa marcadores para cada valor, incluidos los números y los valores que "vienen de tu propio código".
Obtener filas: fetch, fetchAll y fetchColumn
<?php
// One row
$stmt = $pdo->prepare('SELECT * FROM users WHERE id = ?');
$stmt->execute([42]);
$user = $stmt->fetch(); // ['id' => 42, 'name' => 'Ada', ...] or false
if ($user === false) {
http_response_code(404);
}
// All rows
$rows = $pdo->query('SELECT id, name FROM users ORDER BY name')->fetchAll();
// Loop row by row without building an array of all rows
$stmt = $pdo->query('SELECT id, name FROM users');
foreach ($stmt as $row) {
echo $row['name'], "\n";
}
// One value
$count = $pdo->query('SELECT COUNT(*) FROM users')->fetchColumn();
// Two columns as key => value
$names = $pdo->query('SELECT id, name FROM users')->fetchAll(PDO::FETCH_KEY_PAIR); // [42 => 'Ada', ...]
query() está bien cuando el SQL no contiene valores de fuera. En cuanto interviene una variable, pasa a prepare().
Insertar, actualizar y borrar
<?php
$stmt = $pdo->prepare('INSERT INTO users (email, name, password_hash) VALUES (?, ?, ?)');
$stmt->execute([$email, $name, password_hash($password, PASSWORD_DEFAULT)]);
$id = (int) $pdo->lastInsertId();
$stmt = $pdo->prepare('UPDATE users SET name = ? WHERE id = ?');
$stmt->execute([$newName, $id]);
echo $stmt->rowCount(); // rows changed
$stmt = $pdo->prepare('DELETE FROM sessions WHERE expires_at < NOW()');
$stmt->execute();
lastInsertId() devuelve una cadena, así que conviértela. Con MySQL, rowCount() después de un UPDATE cuenta las filas que cambiaron realmente, así que asignar a un nombre el valor que ya tenía da 0. Guarda las contraseñas con password_hash, nunca en texto plano.
Transacciones
Una transacción hace que varias sentencias funcionen o fallen juntas. Si algo lanza una excepción, deshaz los cambios y no se guarda nada:
<?php
try {
$pdo->beginTransaction();
$pdo->prepare('UPDATE accounts SET balance = balance - ? WHERE id = ?')->execute([100, $from]);
$pdo->prepare('UPDATE accounts SET balance = balance + ? WHERE id = ?')->execute([100, $to]);
$pdo->prepare('INSERT INTO transfers (from_id, to_id, amount) VALUES (?, ?, ?)')->execute([$from, $to, 100]);
$pdo->commit();
} catch (Throwable $e) {
if ($pdo->inTransaction()) {
$pdo->rollBack();
}
throw $e;
}
Esto depende de que los errores lancen excepciones (el comportamiento por defecto desde PHP 8.0), para que una sentencia fallida salte al catch. Con MySQL, las tablas deben usar InnoDB; MyISAM ignora las transacciones. La página de try/catch explica Throwable y cómo relanzar.
WHERE IN con un array
Un solo ? contiene un solo valor, así que IN (?) con un array no funciona. Construye un marcador por cada valor:
<?php
$ids = [3, 8, 15];
if ($ids) {
$placeholders = implode(',', array_fill(0, count($ids), '?')); // "?,?,?"
$stmt = $pdo->prepare("SELECT * FROM products WHERE id IN ($placeholders)");
$stmt->execute($ids);
$products = $stmt->fetchAll();
} else {
$products = []; // IN () would be a syntax error
}
La cadena interpolada en el SQL solo contiene signos de interrogación y comas, así que la consulta sigue siendo segura.
Errores frecuentes con los marcadores
- Nombres de tablas y columnas.
ORDER BY ?ordena por una cadena constante, no por la columna. Elige los identificadores de una lista permitida:$sort = in_array($_GET['sort'] ?? '', ['name', 'price'], true) ? $_GET['sort'] : 'name';, y luego pon$sorten el SQL. LIMIT ?con preparaciones emuladas. Con elATTR_EMULATE_PREPARESpor defecto en MySQL,execute(['10'])envíaLIMIT '10', un error de sintaxis. Vincúlalo conPDO::PARAM_INTo desactiva la emulación.- El mismo marcador con nombre dos veces.
WHERE first = :q OR last = :qsolo funciona con preparaciones emuladas. Con preparaciones reales obtienesSQLSTATE[HY093]: Invalid parameter number; usa:q1y:q2. - Olvidar que
fetch()devuelvefalse.$user['name']sobrefalsees un warning ynull; comprueba antes el resultado.
Preguntas frecuentes
¿Cómo me conecto a MySQL con PDO?
Crea un objeto PDO con un DSN, un usuario y una contraseña: $pdo = new PDO('mysql:host=localhost;dbname=shop;charset=utf8mb4', 'user', 'secret', [PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC]);. Una conexión fallida lanza una PDOException; captúrala, registra el mensaje y muestra al visitante un error genérico.
¿Las sentencias preparadas evitan la inyección SQL?
Sí, para los valores: con prepare('... WHERE id = ?') y execute([$id]) el valor se envía como dato y nunca puede cambiar la consulta. Los marcadores no pueden representar nombres de tablas, nombres de columnas ni ASC/DESC, así que compruébalos contra una lista de nombres permitidos antes de ponerlos en el SQL.
¿Cuál es la diferencia entre fetch y fetchAll en PDO?
fetch() devuelve la siguiente fila (o false cuando no quedan más), así que encaja con una sola fila o con un bucle sobre un resultado grande. fetchAll() devuelve todas las filas en un array de una vez. fetchColumn() devuelve un valor de la siguiente fila, práctico para SELECT COUNT(*).
¿Cómo uso WHERE IN con un array en PDO?
Construye un ? por cada valor y pasa el array a execute(): $in = implode(',', array_fill(0, count($ids), '?')); $stmt = $pdo->prepare("SELECT * FROM users WHERE id IN ($in)"); $stmt->execute($ids);. Comprueba antes que el array no esté vacío, porque IN () es un error de sintaxis.
¿Uso PDO o mysqli?
Usa PDO salvo que necesites una función exclusiva de MySQL. Su API es la misma para MySQL, PostgreSQL, SQLite y otros, admite marcadores con nombre y, desde PHP 8.0, lanza excepciones ante los errores por defecto. mysqli solo funciona con MySQL y MariaDB.