Skip to content

Conexión PDO reutilizable ​

Una sola conexión a la base de datos, creada sólo cuando se necesita y compartida por todo el proyecto. Las credenciales viven en un archivo .env, fuera del código.

Para qué sirve ​

  • No repites new PDO(...) en cada archivo.
  • Cambias de servidor (local → hosting) editando sólo el .env.
  • PDO queda configurado con las opciones correctas: excepciones, arrays asociativos y consultas preparadas reales.

Archivos ​

env
# Configuración de la aplicación (NO subir a Git)
APP_ENV=local
APP_URL=http://localhost/mi-proyecto

DB_HOST=127.0.0.1
DB_PORT=3306
DB_NAME=mi_proyecto
DB_USER=root
DB_PASS=
php
<?php
/**
 * Lector mínimo de archivos .env (sin Composer).
 * Formato: CLAVE=valor  — admite comentarios con # y comillas.
 */
function cargarEnv(string $ruta): void
{
    if (!is_file($ruta)) {
        return; // En producción las variables pueden venir del servidor
    }
    foreach (file($ruta, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) as $linea) {
        $linea = trim($linea);
        if ($linea === '' || str_starts_with($linea, '#') || !str_contains($linea, '=')) {
            continue;
        }
        [$clave, $valor] = array_map('trim', explode('=', $linea, 2));
        // Quita comillas simples o dobles alrededor del valor
        $valor = trim($valor, "\"'");
        $_ENV[$clave] = $valor;
        putenv("$clave=$valor");
    }
}

/** Obtiene una variable de entorno con valor por defecto. */
function env(string $clave, mixed $defecto = null): mixed
{
    $valor = $_ENV[$clave] ?? getenv($clave);
    if ($valor === false || $valor === null) {
        return $defecto;
    }
    return match (strtolower((string) $valor)) {
        'true'  => true,
        'false' => false,
        'null'  => null,
        default => $valor,
    };
}

cargarEnv(dirname(__DIR__) . '/.env');
php
<?php
require_once __DIR__ . '/env.php';

/**
 * Conexión PDO única (patrón singleton).
 * Uso: $pdo = Database::get();  o el atajo  db();
 */
final class Database
{
    private static ?PDO $pdo = null;

    // Nadie puede crear instancias: sólo se usa Database::get()
    private function __construct() {}

    public static function get(): PDO
    {
        if (self::$pdo === null) {
            $dsn = sprintf(
                'mysql:host=%s;port=%s;dbname=%s;charset=utf8mb4',
                env('DB_HOST', '127.0.0.1'),
                env('DB_PORT', '3306'),
                env('DB_NAME', '')
            );
            try {
                self::$pdo = new PDO($dsn, env('DB_USER', 'root'), env('DB_PASS', ''), [
                    PDO::ATTR_ERRMODE            => PDO::ERRMODE_EXCEPTION, // errores como excepciones
                    PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,       // arrays asociativos
                    PDO::ATTR_EMULATE_PREPARES   => false,                  // preparadas reales
                ]);
            } catch (PDOException $e) {
                // Nunca muestres el mensaje real al usuario: puede revelar credenciales
                error_log('Error de conexión: ' . $e->getMessage());
                http_response_code(500);
                exit(env('APP_ENV') === 'local' ? $e->getMessage() : 'Error interno. Intenta más tarde.');
            }
        }
        return self::$pdo;
    }
}

/** Atajo global */
function db(): PDO
{
    return Database::get();
}

Cómo usarla ​

php
<?php
require __DIR__ . '/includes/db.php';

// SELECT con parámetros (siempre preparadas, nunca concatenes variables)
$stmt = db()->prepare('SELECT id, nombre, precio FROM productos WHERE categoria_id = ? AND activo = 1');
$stmt->execute([$_GET['cat'] ?? 0]);
$productos = $stmt->fetchAll();

// Una sola fila
$stmt = db()->prepare('SELECT * FROM usuarios WHERE email = :email LIMIT 1');
$stmt->execute(['email' => 'ana@correo.com']);
$usuario = $stmt->fetch(); // false si no existe

// INSERT y obtener el id generado
$stmt = db()->prepare('INSERT INTO productos (nombre, precio) VALUES (?, ?)');
$stmt->execute(['Taza', 129.50]);
$nuevoId = (int) db()->lastInsertId();

// Transacción: todo o nada
try {
    db()->beginTransaction();
    db()->prepare('UPDATE cuentas SET saldo = saldo - ? WHERE id = ?')->execute([100, 1]);
    db()->prepare('UPDATE cuentas SET saldo = saldo + ? WHERE id = ?')->execute([100, 2]);
    db()->commit();
} catch (Throwable $e) {
    db()->rollBack();
    throw $e;
}

Paso a paso para integrarla ​

  1. Crea la carpeta includes/ y pega env.php y db.php.
  2. Crea .env en la raíz con tus datos locales.
  3. Añade .env a .gitignore y crea un .env.example sin contraseñas para tu equipo.
  4. Protege .env e includes/ en Apache:
apache
# .htaccess en la raíz
<Files ".env">
    Require all denied
</Files>
RedirectMatch 403 ^/includes/
  1. En cada página que use la base de datos: require __DIR__ . '/includes/db.php';.

Buena práctica

Lo ideal es que .env e includes/ estén fuera de la carpeta pública (public_html). Si tu hosting lo permite, coloca el proyecto en /home/usuario/app y sólo los archivos públicos en public_html.

Error común

Usar charset=utf8 en lugar de utf8mb4. Con utf8 los emojis y algunos caracteres se guardan como ????. Crea también las tablas con utf8mb4_unicode_ci.

¿Y si uso PostgreSQL o SQLite?

Sólo cambia el DSN:

php
// PostgreSQL
$dsn = sprintf('pgsql:host=%s;port=%s;dbname=%s', env('DB_HOST'), env('DB_PORT', '5432'), env('DB_NAME'));
// SQLite (archivo local)
$dsn = 'sqlite:' . dirname(__DIR__) . '/database.sqlite';

Academia Web — aprende, copia y construye.