Kodokon kodokon.com

PDO: sentencias preparadas y transacciones

Configura PDO como se hace en producción, elimina estructuralmente la inyección SQL y haz fiables tus escrituras con transacciones.

10 min · 3 preguntas

Abrir esta lección en Kodokon

PDO es la interfaz unificada de acceso a bases de datos: MySQL, PostgreSQL o SQLite se manejan todas con la misma API. Tres ajustes separan una conexión ingenua de una de producción: ERRMODE_EXCEPTION para que cada error SQL lance una excepción en lugar de fallar en silencio, FETCH_ASSOC por defecto para obtener resultados limpios, y EMULATE_PREPARES puesto en false para que las sentencias preparadas las ejecute realmente el servidor. Añade charset=utf8mb4 directamente en el DSN - es el único lugar fiable para declararlo.

PHP
<?php

declare(strict_types=1);

$dsn = 'mysql:host=localhost;dbname=shop'
    . ';charset=utf8mb4';

$options = [
    PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
    PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
    PDO::ATTR_EMULATE_PREPARES => false,
];

$pdo = new PDO($dsn, 'app_user', 'secret', $options);
Conexión de producción: excepciones, fetch asociativo, preparación nativa.

La sentencia preparada es tu única defensa estructural contra la inyección SQL. El principio: la estructura de la consulta, con sus marcadores :email o ?, se envía primero al servidor; los datos llegan por separado y nunca se interpretan como SQL. Concatenar una variable en una consulta, aunque esté "escapada", sigue siendo un error de profesional. Los marcadores con nombre hacen el código legible; los posicionales bastan para consultas cortas.

PHP
<?php

declare(strict_types=1);

$pdo = new PDO('sqlite::memory:');
$pdo->setAttribute(
    PDO::ATTR_ERRMODE,
    PDO::ERRMODE_EXCEPTION,
);

$pdo->exec(
    'CREATE TABLE users (
        id INTEGER PRIMARY KEY,
        email TEXT NOT NULL
    )'
);

$insert = $pdo->prepare(
    'INSERT INTO users (email) VALUES (:email)'
);
$insert->execute(['email' => 'lea@example.com']);

$query = $pdo->prepare(
    'SELECT id, email FROM users
     WHERE email = :email'
);
$query->execute(['email' => 'lea@example.com']);

var_dump($query->fetch(PDO::FETCH_ASSOC));
Ejemplo autónomo con SQLite: preparación, vinculación, ejecución.

Una transacción garantiza la atomicidad: o todas las escrituras tienen éxito, o ninguna. El patrón canónico: beginTransaction(), el trabajo dentro de un try, commit() al final del bloque, rollBack() en el catch - y luego relanzas la excepción, porque ocultar el fallo sería peor que el propio fallo. Mantén las transacciones cortas: cada bloqueo retenido durante una llamada de red o un proceso largo degrada la concurrencia de toda la aplicación.

PHP
<?php

declare(strict_types=1);

$pdo = new PDO('sqlite::memory:');
$pdo->setAttribute(
    PDO::ATTR_ERRMODE,
    PDO::ERRMODE_EXCEPTION,
);

$pdo->exec(
    'CREATE TABLE accounts (
        id INTEGER PRIMARY KEY,
        balance INTEGER NOT NULL
    )'
);
$pdo->exec(
    'INSERT INTO accounts (balance)
     VALUES (100), (20)'
);

$pdo->beginTransaction();

try {
    $debit = $pdo->prepare(
        'UPDATE accounts
         SET balance = balance - :n
         WHERE id = :id'
    );
    $debit->execute(['n' => 50, 'id' => 1]);

    $credit = $pdo->prepare(
        'UPDATE accounts
         SET balance = balance + :n
         WHERE id = :id'
    );
    $credit->execute(['n' => 50, 'id' => 2]);

    $pdo->commit();
} catch (Throwable $e) {
    $pdo->rollBack();
    throw $e;
}

echo 'Transfer done';
Transferencia atómica: todo tiene éxito, o todo se revierte.

Prueba de conocimientos

Comprueba que has retenido los puntos clave de esta lección.

  1. ¿Por qué una sentencia preparada bloquea la inyección SQL?
    • Escapa automáticamente los apóstrofos en las cadenas
    • La estructura SQL y los datos viajan por separado: los datos nunca se interpretan como código SQL
    • Cifra la consulta entre PHP y el servidor
  2. ¿Qué puedes pasar a través de un marcador :param en una sentencia preparada?
    • Un valor: cadena, número o null
    • Un nombre de tabla o de columna
    • Una cláusula ORDER BY completa
  3. En el patrón canónico de transacción, ¿qué haces en el bloque catch?
    • commit() para guardar lo que tuvo éxito
    • rollBack(), y luego relanzar la excepción
    • Nada: la transacción caduca por sí sola