Cómo interactuar con una base de datos MySQL usando PHP 8 y PDO (actualizado 2026)

Artículo original de febrero de 2002. Actualizado en mayo de 2026 por David Carrero. Las funciones mysql_* que se usaban en el original fueron eliminadas definitivamente en PHP 7.0 (diciembre de 2015). Este artículo conserva el enfoque original para contexto histórico y añade la versión moderna con PDO.

PHP y MySQL: una combinación que sigue dominando la web

En 2002, cuando se escribió este artículo, la combinación de PHP y MySQL era ya la opción más popular para crear sitios dinámicos. Dos décadas después sigue siéndolo: PHP mueve el 77% de los servidores con lenguaje detectado, y MySQL es el motor de bases de datos de código abierto más usado del mundo. La diferencia está en cómo se hace la conexión.

El código original usaba las funciones mysql_* de PHP 4: mysql_connect(), mysql_query(), mysql_fetch_row(). Esas funciones se marcaron como obsoletas en PHP 5.5 (2013) y se eliminaron por completo en PHP 7.0 (diciembre de 2015). Si tu código todavía las usa, simplemente no funciona en ningún servidor moderno.

La sustitución oficial son MySQLi (extensión mejorada, solo MySQL) o PDO (agnóstica de base de datos). En este artículo usaremos PDO porque funciona con MySQL, PostgreSQL, SQLite y otros motores sin cambiar la lógica de acceso a datos.

La estructura de la base de datos (igual que en 2002)

Seguimos con el ejemplo original: una tabla usuarios sencilla. Lo que ha cambiado es cómo se crea y cómo se accede a ella.

CREATE DATABASE IF NOT EXISTS ejemplo CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
USE ejemplo;

CREATE TABLE IF NOT EXISTS usuarios (
    id        INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    nombre    VARCHAR(50)  NOT NULL,
    apellido  VARCHAR(50)  NOT NULL,
    dni       VARCHAR(20)  NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

Dos diferencias respecto al original: usamos utf8mb4 (soporte completo para Unicode, incluidos emojis) e InnoDB como motor de almacenamiento, que admite transacciones y claves foráneas.

Código histórico de 2002 (solo referencia)

El artículo original usaba cinco archivos con las funciones mysql_*. Lo reproducimos aquí para comparar, pero no lo uses en proyectos reales:

<?php
// conexion.php - PHP 4/5, NO compatible con PHP 7+
$dbhost     = "localhost";
$dbusuario  = "agustin";
$dbpassword = "mipass";
$db         = "ejemplo";

$conexion = mysql_connect($dbhost, $dbusuario, $dbpassword); // Eliminado en PHP 7.0
mysql_select_db($db, $conexion);
?>

El problema más grave del código original no era solo usar funciones obsoletas: los valores del formulario se insertaban directamente en las consultas sin escapar ni validar, lo que abría una inyección SQL clásica. Por ejemplo:

<?php
// Código vulnerable del original (no usar)
$result = mysql_query("INSERT INTO usuarios (id, nombre, apellido, dni)
    VALUES ('', $nombre, $apellido, $dni)", $conexion);
?>

Si $nombre valía x', '', ''); DROP TABLE usuarios; --, la consulta eliminaba la tabla completa. Con PDO y sentencias preparadas ese ataque es imposible por diseño.

Versión moderna con PDO (PHP 8)

Conexión reutilizable

<?php
// conexion.php — PHP 8 con PDO
function obtenerConexion(): PDO
{
    static $pdo = null;

    if ($pdo === null) {
        $pdo = new PDO(
            'mysql:host=localhost;dbname=ejemplo;charset=utf8mb4',
            'agustin',
            'mipass',
            [
                PDO::ATTR_ERRMODE            => PDO::ERRMODE_EXCEPTION,
                PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
                PDO::ATTR_EMULATE_PREPARES   => false,
            ]
        );
    }

    return $pdo;
}
?>

La instancia se crea solo una vez gracias a la variable estática. PDO::ATTR_EMULATE_PREPARES => false garantiza que las sentencias preparadas son preparadas de verdad en el servidor MySQL, no simuladas por el driver PHP.

Guardar un registro nuevo

<?php
require 'conexion.php';

function guardarUsuario(string $nombre, string $apellido, string $dni): int
{
    $pdo = obtenerConexion();
    $stmt = $pdo->prepare(
        "INSERT INTO usuarios (nombre, apellido, dni) VALUES (:nombre, :apellido, :dni)"
    );
    $stmt->execute([':nombre' => $nombre, ':apellido' => $apellido, ':dni' => $dni]);
    return (int) $pdo->lastInsertId();
}

// Formulario de alta
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $nombre   = trim($_POST['nombre']   ?? '');
    $apellido = trim($_POST['apellido'] ?? '');
    $dni      = trim($_POST['dni']      ?? '');

    if ($nombre && $apellido && $dni) {
        $id = guardarUsuario($nombre, $apellido, $dni);
        echo "<p>Usuario guardado con ID: $id</p>";
    }
}
?>
<form method="post">
  <label>Nombre: <input type="text" name="nombre" required></label><br>
  <label>Apellido: <input type="text" name="apellido" required></label><br>
  <label>DNI: <input type="text" name="dni" required></label><br>
  <button type="submit">Guardar</button>
</form>

Ver todos los registros

<?php
require 'conexion.php';

function listarUsuarios(): array
{
    return obtenerConexion()
        ->query("SELECT id, nombre, apellido, dni FROM usuarios ORDER BY apellido, nombre")
        ->fetchAll();
}

$usuarios = listarUsuarios();
?>
<table>
  <thead>
    <tr><th>Nombre</th><th>Apellido</th><th>DNI</th><th></th></tr>
  </thead>
  <tbody>
  <?php foreach ($usuarios as $u): ?>
    <tr>
      <td><?= htmlspecialchars($u['nombre']) ?></td>
      <td><?= htmlspecialchars($u['apellido']) ?></td>
      <td><?= htmlspecialchars($u['dni']) ?></td>
      <td><a href="actualizar.php?id=<?= $u['id'] ?>">Editar</a></td>
    </tr>
  <?php endforeach; ?>
  </tbody>
</table>

Fíjate en htmlspecialchars() al renderizar los datos. Aunque los hemos guardado correctamente, siempre hay que escapar los valores al imprimirlos en HTML para evitar XSS.

Actualizar un registro

<?php
require 'conexion.php';

function obtenerUsuario(int $id): ?array
{
    $stmt = obtenerConexion()->prepare(
        "SELECT id, nombre, apellido, dni FROM usuarios WHERE id = :id"
    );
    $stmt->execute([':id' => $id]);
    $row = $stmt->fetch();
    return $row ?: null;
}

function actualizarUsuario(int $id, string $nombre, string $apellido, string $dni): bool
{
    $stmt = obtenerConexion()->prepare(
        "UPDATE usuarios SET nombre=:nombre, apellido=:apellido, dni=:dni WHERE id=:id"
    );
    $stmt->execute([':nombre' => $nombre, ':apellido' => $apellido, ':dni' => $dni, ':id' => $id]);
    return $stmt->rowCount() > 0;
}

$id = (int) ($_GET['id'] ?? 0);
if (!$id) { header('Location: ver.php'); exit; }

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $ok = actualizarUsuario(
        $id,
        trim($_POST['nombre']),
        trim($_POST['apellido']),
        trim($_POST['dni'])
    );
    if ($ok) { header('Location: ver.php'); exit; }
}

$usuario = obtenerUsuario($id);
if (!$usuario) { http_response_code(404); echo 'Usuario no encontrado'; exit; }
?>
<form method="post">
  <label>Nombre: <input type="text" name="nombre" value="<?= htmlspecialchars($usuario['nombre']) ?>" required></label><br>
  <label>Apellido: <input type="text" name="apellido" value="<?= htmlspecialchars($usuario['apellido']) ?>" required></label><br>
  <label>DNI: <input type="text" name="dni" value="<?= htmlspecialchars($usuario['dni']) ?>" required></label><br>
  <button type="submit">Guardar cambios</button>
</form>

Eliminar un registro

El artículo original no incluía la operación de borrado. La añadimos aquí para completar el CRUD:

<?php
require 'conexion.php';

function eliminarUsuario(int $id): bool
{
    $stmt = obtenerConexion()->prepare("DELETE FROM usuarios WHERE id = :id");
    $stmt->execute([':id' => $id]);
    return $stmt->rowCount() > 0;
}

// Solo aceptar POST para operaciones destructivas
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $id = (int) ($_POST['id'] ?? 0);
    if ($id && eliminarUsuario($id)) {
        header('Location: ver.php');
        exit;
    }
}
?>

Gestión de errores

PDO lanza excepciones cuando algo falla, así que puedes capturarlas donde las necesites:

<?php
try {
    $id = guardarUsuario('Ana', 'López', '12345678A');
    echo "Guardado con ID: $id";
} catch (PDOException $e) {
    // En producción: log($e->getMessage()), mostrar mensaje genérico
    error_log('DB error: ' . $e->getMessage());
    echo 'Ha ocurrido un error al guardar los datos.';
}
?>

Nunca muestres el mensaje de la excepción directamente al usuario en producción: puede revelar la estructura de tu base de datos.

Del código de 2002 a PHP 8: resumen de diferencias

PHP 4/5 (original)

PHP 8 con PDO

mysql_connect()

new PDO(...)

Variables directas en SQL

Sentencias preparadas con :param

Sin protección contra SQL injection

Imposible por diseño

Error silencioso o die()

Excepciones PDOException

Solo MySQL

MySQL, PostgreSQL, SQLite, Oracle…

Eliminado en PHP 7.0 (2015)

Soportado en PHP 5.1+ y PHP 8

Si buscas cómo almacenar imágenes o ficheros binarios en MySQL con PHP 8, consulta el artículo Manejo de datos BLOB con PHP y MySQL. Para proteger las contraseñas de tus usuarios, lee Contraseñas seguras en PHP: de MD5 a bcrypt y Argon2.

Imagen: Pexels / Pixabay

COMPARTE ESTE ARTÍCULO

COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN LINKEDIN
COMPARTIR EN WHATSAPP
SIGUIENTE ARTÍCULO