PDO en PHP y prepared statements: prevenir SQL injection de forma correcta

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.

COMPARTE ESTE ARTÍCULO

COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN LINKEDIN
COMPARTIR EN WHATSAPP