EJEMPLO PRÁCTICO: Conexión a BD con PHP-PDO

Este ejemplo está diseñado para un nivel básico/intermedio y muestra cómo implementar una conexión segura a bases de datos usando PDO, incluyendo validaciones, manejo de errores y buenas prácticas.

🧠 Conceptos Fundamentales

🔧 PDO (PHP Data Objects)

PDO es una capa de abstracción para acceder a bases de datos en PHP que ofrece:

📌 Ventajas de PDO sobre funciones mysql_*

🔍 ¿Cuándo usar PDO vs MySQLi?

PDO es mejor cuando:

  • Necesitas trabajar con múltiples tipos de bases de datos
  • Quieres usar sentencias preparadas con parámetros con nombre
  • Prefieres el manejo de errores por excepciones

MySQLi es mejor cuando:

  • Trabajas exclusivamente con MySQL/MariaDB
  • Necesitas características específicas de MySQL como replicación
  • Prefieres la API procedural en lugar de orientada a objetos

Tecnologías utilizadas

PDO (PHP Data Objects) SQL (Lenguaje de consulta estructurado) PHP 7+ HTML5 CSS3 JavaScript ES6 AJAX

Herramientas necesarias

Servidor local (XAMPP, MAMP, LAMP) Navegador web moderno Editor de código (VS Code, PHPStorm) MySQL 5.6+ o MariaDB PHP 7.2+ phpMyAdmin (opcional)

📝 Enunciado del Ejercicio

Desarrollar una aplicación web que permita:

🏆 Buenas Prácticas

Usar sentencias preparadas: Siempre utiliza parámetros vinculados para prevenir inyección SQL.
Validar y sanitizar: Valida en el cliente para UX, pero siempre valida y sanitiza en el servidor.
Manejo de errores: Usa try-catch para PDOException y registra errores sin mostrar detalles al usuario.
Conexiones seguras: Usa conexiones cifradas (SSL) en producción y nunca almacenes credenciales en el código.
UTF-8: Configura el charset a utf8mb4 para soportar todos los caracteres Unicode.
🔎 ¿Qué más debería considerar para una aplicación profesional?
  • Autenticación segura: Usa password_hash() para contraseñas
  • CSRF Protection: Implementa tokens contra Cross-Site Request Forgery
  • Rate Limiting: Limita intentos de registro para prevenir abuso
  • CAPTCHA: Considera añadir CAPTCHA para formularios públicos
  • Logging: Registra actividades importantes para auditoría
  • Backups: Implementa un sistema de backup para la base de datos

📌 Pasos para Implementar

Configura el entorno: Instala XAMPP/MAMP/WAMP o configura un entorno de desarrollo con PHP y MySQL.
Crea la base de datos: Usa phpMyAdmin o la línea de comandos para crear una base de datos y usuario con privilegios.
Configura las credenciales: Modifica el archivo conexion.php con tus datos de conexión.
Prueba la conexión: Crea un archivo test.php para verificar que la conexión PDO funciona.
Implementa el formulario: Copia el código HTML/JS en tu archivo y los scripts PHP en sus respectivos archivos.
Pruebas: Verifica que el formulario valide correctamente y que los datos se almacenen en la BD.
Mejoras: Añade características como listado de usuarios, edición o paginación.

Códigos a Implementar

conexion.php
// Configuración de la conexión PDO
<?php
// Definimos constantes para la configuración
define('DB_HOST', 'localhost');
define('DB_NAME', 'mi_base_datos');
define('DB_USER', 'usuario_seguro');
define('DB_PASS', 'contraseña_compleja');

try {
    // Creamos la conexión PDO
    $conn = new PDO(
        "mysql:host=".DB_HOST.";dbname=".DB_NAME.";charset=utf8mb4", 
        DB_USER, 
        DB_PASS,
        [
            PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
            PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
            PDO::ATTR_EMULATE_PREPARES => false
        ]
    );
    
    // Verificamos y creamos la tabla si no existe
    $sql = "CREATE TABLE IF NOT EXISTS usuarios (
        id INT AUTO_INCREMENT PRIMARY KEY,
        nombre VARCHAR(100) NOT NULL,
        email VARCHAR(255) NOT NULL UNIQUE,
        fecha_registro DATETIME DEFAULT CURRENT_TIMESTAMP,
        INDEX idx_email (email)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4";
    
    $conn->exec($sql);
    
} catch(PDOException $e) {
    error_log("Error de conexión: " . $e->getMessage());
    die("Ocurrió un error al conectar con la base de datos.");
}
?>
registrar_usuario.php
<?php
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
    http_response_code(405);
    exit('Método no permitido');
}

header('Content-Type: application/json');

try {
    require_once 'conexion.php';
    
    $nombre = filter_input(INPUT_POST, 'nombre', FILTER_SANITIZE_STRING);
    $email = filter_input(INPUT_POST, 'email', FILTER_SANITIZE_EMAIL);
    
    if (empty($nombre) || empty($email)) {
        throw new Exception('Todos los campos son obligatorios');
    }
    
    if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
        throw new Exception('El correo electrónico no es válido');
    }
    
    $stmt = $conn->prepare("INSERT INTO usuarios (nombre, email) VALUES (:nombre, :email)");
    $stmt->bindParam(':nombre', $nombre, PDO::PARAM_STR);
    $stmt->bindParam(':email', $email, PDO::PARAM_STR);
    
    if ($stmt->execute()) {
        echo json_encode([
            'success' => true,
            'message' => 'Usuario registrado correctamente',
            'id' => $conn->lastInsertId()
        ]);
    } else {
        throw new Exception('Error al ejecutar la consulta');
    }
    
} catch (PDOException $e) {
    if ($e->getCode() == 23000) {
        echo json_encode([
            'success' => false,
            'message' => 'El correo electrónico ya está registrado'
        ]);
    } else {
        error_log("Error PDO: " . $e->getMessage());
        echo json_encode([
            'success' => false,
            'message' => 'Error en la base de datos.'
        ]);
    }
} catch (Exception $e) {
    echo json_encode([
        'success' => false,
        'message' => $e->getMessage()
    ]);
}
?>
script.js
document.addEventListener('DOMContentLoaded', function() {
    const form = document.getElementById('registroForm');
    const mensajeDiv = document.getElementById('mensaje');
    
    function validarNombre() {
        const nombre = document.getElementById('nombre').value.trim();
        const errorElement = document.getElementById('errorNombre');
        
        if (nombre === '') {
            errorElement.textContent = 'El nombre es obligatorio';
            return false;
        } else if (nombre.length < 3) {
            errorElement.textContent = 'El nombre debe tener al menos 3 caracteres';
            return false;
        } else {
            errorElement.textContent = '';
            return true;
        }
    }
    
    function validarEmail() {
        const email = document.getElementById('email').value.trim();
        const errorElement = document.getElementById('errorEmail');
        const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
        
        if (email === '') {
            errorElement.textContent = 'El correo electrónico es obligatorio';
            return false;
        } else if (!emailRegex.test(email)) {
            errorElement.textContent = 'Ingresa un correo electrónico válido';
            return false;
        } else {
            errorElement.textContent = '';
            return true;
        }
    }
    
    form.addEventListener('submit', async function(e) {
        e.preventDefault();
        mensajeDiv.style.display = 'none';
        
        const nombreValido = validarNombre();
        const emailValido = validarEmail();
        
        if (!nombreValido || !emailValido) {
            mostrarMensaje('Por favor, corrige los errores en el formulario', false);
            return;
        }
        
        const formData = new FormData(form);
        
        try {
            const response = await fetch('registrar_usuario.php', {
                method: 'POST',
                body: formData
            });
            
            const data = await response.json();
            
            if (data.success) {
                mostrarMensaje(data.message, true);
                form.reset();
            } else {
                mostrarMensaje(data.message, false);
            }
            
        } catch (error) {
            console.error('Error:', error);
            mostrarMensaje('Error al conectar con el servidor', false);
        }
    });
    
    function mostrarMensaje(texto, esExito) {
        mensajeDiv.textContent = texto;
        mensajeDiv.className = 'mensaje ' + (esExito ? 'exito' : 'error-mensaje');
        mensajeDiv.style.display = 'block';
        mensajeDiv.scrollIntoView({ behavior: 'smooth' });
    }
});
tutorial_completo.php
/**
 * ARCHIVO COMPLETO DE TUTORIAL PDO PHP
 * 
 * Este archivo contiene todo el código necesario para el tutorial de conexión a bases de datos
 * usando PDO en PHP. Incluye la conexión, registro de usuarios y validación.
 * 
 * Estructura:
 * 1. Configuración de conexión PDO
 * 2. Procesamiento del formulario
 * 3. HTML con formulario
 * 4. JavaScript para validación
 */

<?php
// =============================================
// SECCIÓN 1: CONFIGURACIÓN DE LA CONEXIÓN PDO
// =============================================
define('DB_HOST', 'localhost');
define('DB_NAME', 'mi_base_datos');
define('DB_USER', 'usuario_seguro');
define('DB_PASS', 'contraseña_compleja');

try {
    // Crear conexión PDO con opciones de configuración
    $conn = new PDO(
        "mysql:host=".DB_HOST.";dbname=".DB_NAME.";charset=utf8mb4", 
        DB_USER, 
        DB_PASS,
        [
            PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
            PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
            PDO::ATTR_EMULATE_PREPARES => false
        ]
    );
    
    // Crear tabla de usuarios si no existe
    $sql = "CREATE TABLE IF NOT EXISTS usuarios (
        id INT AUTO_INCREMENT PRIMARY KEY,
        nombre VARCHAR(100) NOT NULL,
        email VARCHAR(255) NOT NULL UNIQUE,
        fecha_registro DATETIME DEFAULT CURRENT_TIMESTAMP,
        INDEX idx_email (email)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4";
    
    $conn->exec($sql);

} catch(PDOException $e) {
    error_log("Error de conexión: " . $e->getMessage());
    die("Ocurrió un error al conectar con la base de datos.");
}

// =============================================
// SECCIÓN 2: PROCESAMIENTO DEL FORMULARIO
// =============================================
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    header('Content-Type: application/json');
    
    try {
        // Sanitizar y validar datos de entrada
        $nombre = filter_input(INPUT_POST, 'nombre', FILTER_SANITIZE_STRING);
        $email = filter_input(INPUT_POST, 'email', FILTER_SANITIZE_EMAIL);
        
        if (empty($nombre) || empty($email)) {
            throw new Exception('Todos los campos son obligatorios');
        }
        
        if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
            throw new Exception('El correo electrónico no es válido');
        }
        
        // Insertar usando sentencias preparadas
        $stmt = $conn->prepare("INSERT INTO usuarios (nombre, email) VALUES (:nombre, :email)");
        $stmt->bindParam(':nombre', $nombre, PDO::PARAM_STR);
        $stmt->bindParam(':email', $email, PDO::PARAM_STR);
        
        if ($stmt->execute()) {
            echo json_encode([
                'success' => true,
                'message' => 'Usuario registrado correctamente',
                'id' => $conn->lastInsertId()
            ]);
        } else {
            throw new Exception('Error al ejecutar la consulta');
        }
        
    } catch (PDOException $e) {
        if ($e->getCode() == 23000) {
            echo json_encode([
                'success' => false,
                'message' => 'El correo electrónico ya está registrado'
            ]);
        } else {
            error_log("Error PDO: " . $e->getMessage());
            echo json_encode([
                'success' => false,
                'message' => 'Error en la base de datos.'
            ]);
        }
    } catch (Exception $e) {
        echo json_encode([
            'success' => false,
            'message' => $e->getMessage()
        ]);
    }
    exit;
}
?>

<!-- =============================================
SECCIÓN 3: HTML CON FORMULARIO
============================================= -->
<!DOCTYPE html>
<html lang="es">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Registro de Usuarios con PDO</title>
    <style>
        /* Estilos CSS para el formulario */
        body {
            font-family: 'Arial', sans-serif;
            line-height: 1.6;
            padding: 20px;
            max-width: 600px;
            margin: 0 auto;
        }
        .form-group {
            margin-bottom: 15px;
        }
        .error {
            color: red;
            font-size: 0.9em;
        }
    </style>
</head>
<body>
    <h1>Registro de Usuarios</h1>
    
    <div id="mensaje"></div>
    
    <form id="registroForm">
        <div class="form-group">
            <label for="nombre">Nombre:</label>
            <input type="text" id="nombre" name="nombre" required>
            <div id="errorNombre" class="error"></div>
        </div>
        
        <div class="form-group">
            <label for="email">Email:</label>
            <input type="email" id="email" name="email" required>
            <div id="errorEmail" class="error"></div>
        </div>
        
        <button type="submit">Registrar</button>
    </form>
    
    <!-- =============================================
    SECCIÓN 4: JAVASCRIPT PARA VALIDACIÓN
    ============================================= -->
    <script>
        document.addEventListener('DOMContentLoaded', function() {
            const form = document.getElementById('registroForm');
            const mensajeDiv = document.getElementById('mensaje');
            
            // Validación en tiempo real
            document.getElementById('nombre').addEventListener('input', validarNombre);
            document.getElementById('email').addEventListener('input', validarEmail);
            
            function validarNombre() {
                const nombre = this.value.trim();
                const errorElement = document.getElementById('errorNombre');
                
                if (nombre === '') {
                    errorElement.textContent = 'El nombre es obligatorio';
                    return false;
                } else if (nombre.length < 3) {
                    errorElement.textContent = 'El nombre debe tener al menos 3 caracteres';
                    return false;
                } else {
                    errorElement.textContent = '';
                    return true;
                }
            }
            
            function validarEmail() {
                const email = this.value.trim();
                const errorElement = document.getElementById('errorEmail');
                const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
                
                if (email === '') {
                    errorElement.textContent = 'El correo electrónico es obligatorio';
                    return false;
                } else if (!emailRegex.test(email)) {
                    errorElement.textContent = 'Ingresa un correo electrónico válido';
                    return false;
                } else {
                    errorElement.textContent = '';
                    return true;
                }
            }
            
            // Envío del formulario con AJAX
            form.addEventListener('submit', async function(e) {
                e.preventDefault();
                mensajeDiv.style.display = 'none';
                
                const nombreValido = validarNombre.call(document.getElementById('nombre'));
                const emailValido = validarEmail.call(document.getElementById('email'));
                
                if (!nombreValido || !emailValido) {
                    mostrarMensaje('Por favor, corrige los errores en el formulario', false);
                    return;
                }
                
                const formData = new FormData(form);
                
                try {
                    const response = await fetch('', {
                        method: 'POST',
                        body: formData
                    });
                    
                    const data = await response.json();
                    
                    if (data.success) {
                        mostrarMensaje(data.message, true);
                        form.reset();
                    } else {
                        mostrarMensaje(data.message, false);
                    }
                    
                } catch (error) {
                    console.error('Error:', error);
                    mostrarMensaje('Error al conectar con el servidor', false);
                }
            });
            
            function mostrarMensaje(texto, esExito) {
                mensajeDiv.textContent = texto;
                mensajeDiv.className = 'mensaje ' + (esExito ? 'exito' : 'error-mensaje');
                mensajeDiv.style.display = 'block';
                mensajeDiv.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
            }
        });
    </script>
</body>
</html>
☰   UF2215-Contenidos