PDO (PHP Data Objects) es la extensión nativa de PHP para acceder a bases de datos de forma uniforme e independiente del motor. Su característica más importante son los prepared statements: consultas parametrizadas que separan el código SQL de los datos, eliminando la posibilidad de SQL injection. Este artículo cubre todo lo que necesitas para usar PDO correctamente.
Conectar con PDO
<?php
$dsn = 'mysql:host=localhost;dbname=mi_app;charset=utf8mb4';
try {
$pdo = new PDO($dsn, 'usuario', 'contraseña', [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, // lanzar PDOException
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, // arrays asociativos
PDO::ATTR_EMULATE_PREPARES => false, // prepared statements reales
]);
} catch (PDOException $e) {
// No mostrar el mensaje de error al usuario en producción
error_log('DB connection failed: ' . $e->getMessage());
throw new RuntimeException('No se pudo conectar a la base de datos');
}
?>
Prepared statements: parámetros posicionales y nombrados
<?php
// Parámetros posicionales (?)
$stmt = $pdo->prepare('SELECT * FROM productos WHERE categoria = ? AND precio < ?');
$stmt->execute(['electronica', 500.0]);
$productos = $stmt->fetchAll();
// Parámetros nombrados (:nombre)
$stmt = $pdo->prepare('SELECT * FROM usuarios WHERE email = :email AND activo = :activo');
$stmt->execute([':email' => '[email protected]', ':activo' => 1]);
$usuario = $stmt->fetch();
// Por qué esto es seguro: el valor se envía separado del SQL
// Un atacante no puede romper la consulta con '1; DROP TABLE usuarios --'
$emailMalicioso = "' OR '1'='1";
$stmt->execute([':email' => $emailMalicioso, ':activo' => 1]); // seguro
?>
Modos de fetch
<?php
class Producto
{
public int $id;
public string $nombre;
public float $precio;
}
$stmt = $pdo->query('SELECT id, nombre, precio FROM productos LIMIT 5');
// FETCH_ASSOC: array asociativo
$fila = $stmt->fetch(PDO::FETCH_ASSOC);
// ['id' => 1, 'nombre' => 'Teclado', 'precio' => 89.99]
// FETCH_OBJ: objeto stdClass
$stmt2 = $pdo->query('SELECT id, nombre FROM productos LIMIT 1');
$obj = $stmt2->fetch(PDO::FETCH_OBJ);
echo $obj->nombre;
// FETCH_CLASS: instancia de tu clase
$stmt3 = $pdo->query('SELECT id, nombre, precio FROM productos');
$stmt3->setFetchMode(PDO::FETCH_CLASS, Producto::class);
while ($prod = $stmt3->fetch()) {
echo $prod->nombre . ': ' . $prod->precio . "n";
}
?>
Transacciones
<?php
function transferir(PDO $pdo, int $origenId, int $destinoId, float $cantidad): void
{
$pdo->beginTransaction();
try {
// Retirar del origen
$stmt = $pdo->prepare(
'UPDATE cuentas SET saldo = saldo - :cantidad WHERE id = :id AND saldo >= :cantidad'
);
$stmt->execute([':cantidad' => $cantidad, ':id' => $origenId]);
if ($stmt->rowCount() === 0) {
throw new RuntimeException('Saldo insuficiente o cuenta no encontrada');
}
// Añadir al destino
$stmt2 = $pdo->prepare(
'UPDATE cuentas SET saldo = saldo + :cantidad WHERE id = :id'
);
$stmt2->execute([':cantidad' => $cantidad, ':id' => $destinoId]);
$pdo->commit();
} catch (Throwable $e) {
$pdo->rollBack();
throw $e;
}
}
?>
El error que se debe evitar: concatenación de SQL
<?php
// MAL: SQL injection garantizado
$id = $_GET['id']; // si el usuario envía: 1; DROP TABLE usuarios
$sql = "SELECT * FROM usuarios WHERE id = $id"; // peligroso
$resultado = $pdo->query($sql);
// BIEN: siempre prepared statements
$stmt = $pdo->prepare('SELECT * FROM usuarios WHERE id = :id');
$stmt->execute([':id' => (int)$_GET['id']]);
$usuario = $stmt->fetch();
?>
La documentación oficial de PDO en PHP cubre todos los drivers disponibles (MySQL, PostgreSQL, SQLite), las opciones de conexión, los modos de cursor y el manejo avanzado de errores con información diagnóstica.
