Apuntes sobre Php

PHP: Porque a veces necesitas un poco de caos en tu vida.

Consultas preparadas con mysqli

Las consultas preparadas (prepared statements) son la solución definitiva contra la inyección SQL. En lugar de construir la consulta concatenando valores del usuario, se define primero la estructura con marcadores de posición (?) y se vinculan los valores por separado. MySQL los trata siempre como datos, nunca como código SQL.

Por qué son necesarias

Este código es vulnerable a SQL injection:

<?php

// NUNCA hagas esto con datos del usuario
$email = $_POST["email"];  // ¿Y si el usuario escribe: ' OR '1'='1 ?
mysqli_query($link, "SELECT * FROM usuarios WHERE email = '$email'");

Con prepared statements, el valor siempre se trata como dato literal, sin importar lo que contenga.

SELECT con prepared statement

El flujo es: mysqli_prepare()mysqli_stmt_bind_param()mysqli_stmt_execute()mysqli_stmt_get_result():

<?php

$stmt = mysqli_prepare($link, "SELECT id, nombre, email FROM usuarios WHERE email = ?");

if (!$stmt) {
    die("Error preparando consulta: " . mysqli_error($link));
}

$email = $_POST["email"];
mysqli_stmt_bind_param($stmt, "s", $email);   // "s" = string
mysqli_stmt_execute($stmt);

$resultado = mysqli_stmt_get_result($stmt);
$usuario   = mysqli_fetch_assoc($resultado);

if ($usuario === null) {
    echo "Usuario no encontrado.";
} else {
    echo "Bienvenido, " . htmlspecialchars($usuario["nombre"]);
}

mysqli_stmt_close($stmt);

Tipos en mysqli_stmt_bind_param()

El segundo argumento es una cadena de tipos, uno por cada ? en la consulta:

  • s: string
  • i: integer
  • d: double (decimal)
  • b: blob (datos binarios)
<?php

// Dos parámetros: un string y un entero
$stmt = mysqli_prepare($link, "SELECT * FROM pedidos WHERE email = ? AND estado_id = ?");
mysqli_stmt_bind_param($stmt, "si", $email, $estado_id);

INSERT con prepared statement

<?php

$stmt = mysqli_prepare($link, "INSERT INTO usuarios (nombre, email, hash_clave) VALUES (?, ?, ?)");
mysqli_stmt_bind_param($stmt, "sss", $nombre, $email, $hash);

$nombre = filter_input(INPUT_POST, "nombre", FILTER_SANITIZE_FULL_SPECIAL_CHARS);
$email  = filter_input(INPUT_POST, "email",  FILTER_VALIDATE_EMAIL);
$hash   = password_hash($_POST["contrasena"], PASSWORD_DEFAULT);

if (!mysqli_stmt_execute($stmt)) {
    error_log("Error al insertar: " . mysqli_stmt_error($stmt));
    echo "No se pudo crear el usuario.";
} else {
    echo "Usuario creado con ID: " . mysqli_insert_id($link);
}

mysqli_stmt_close($stmt);

UPDATE y DELETE

<?php

// UPDATE
$stmt = mysqli_prepare($link, "UPDATE usuarios SET nombre = ? WHERE id = ?");
mysqli_stmt_bind_param($stmt, "si", $nombre_nuevo, $usuario_id);
mysqli_stmt_execute($stmt);
echo "Filas actualizadas: " . mysqli_stmt_affected_rows($stmt);
mysqli_stmt_close($stmt);

// DELETE
$stmt = mysqli_prepare($link, "DELETE FROM sesiones WHERE usuario_id = ? AND expira < NOW()");
mysqli_stmt_bind_param($stmt, "i", $usuario_id);
mysqli_stmt_execute($stmt);
mysqli_stmt_close($stmt);

Transacciones

Una transacción agrupa varias operaciones en una unidad atómica: o se ejecutan todas o no se ejecuta ninguna. Imprescindible cuando varias escrituras deben ser consistentes entre sí (por ejemplo, transferencia entre cuentas: restar de una y sumar en otra).

<?php

mysqli_autocommit($link, false);   // desactiva el commit automático

$ok = true;

$stmt1 = mysqli_prepare($link, "UPDATE cuentas SET saldo = saldo - ? WHERE id = ?");
mysqli_stmt_bind_param($stmt1, "di", $importe, $cuenta_origen);
if (!mysqli_stmt_execute($stmt1)) { $ok = false; }
mysqli_stmt_close($stmt1);

$stmt2 = mysqli_prepare($link, "UPDATE cuentas SET saldo = saldo + ? WHERE id = ?");
mysqli_stmt_bind_param($stmt2, "di", $importe, $cuenta_destino);
if (!mysqli_stmt_execute($stmt2)) { $ok = false; }
mysqli_stmt_close($stmt2);

if ($ok) {
    mysqli_commit($link);
    echo "Transferencia completada.";
} else {
    mysqli_rollback($link);
    error_log("Transacción fallida: " . mysqli_error($link));
    echo "Error al procesar la transferencia.";
}

mysqli_autocommit($link, true);   // restaura el modo normal

Nota: Por defecto mysqli está en modo autocommit: cada consulta se confirma inmediatamente al ejecutarse. mysqli_autocommit($link, false) lo desactiva hasta que llames a mysqli_commit() o mysqli_rollback().

Recapitulación

  • Los prepared statements previenen SQL injection: los valores del usuario se tratan siempre como datos, nunca como código.
  • Flujo: mysqli_prepare()mysqli_stmt_bind_param(tipos, vars...)mysqli_stmt_execute(). Para SELECT añade mysqli_stmt_get_result().
  • Tipos en bind_param: s (string), i (int), d (double), b (blob).
  • Cierra los statements con mysqli_stmt_close() cuando termines.
  • Las transacciones (mysqli_autocommit(false) → operaciones → mysqli_commit() o mysqli_rollback()) garantizan que un conjunto de operaciones se ejecute de forma atómica.

En la próxima lección: estructura de un proyecto PHP: organización de carpetas, separación de responsabilidades y seguridad básica.

TOP