Tema
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
- Crea la carpeta
includes/y pegaenv.phpydb.php. - Crea
.enven la raíz con tus datos locales. - Añade
.enva.gitignorey crea un.env.examplesin contraseñas para tu equipo. - Protege
.enveincludes/en Apache:
apache
# .htaccess en la raíz
<Files ".env">
Require all denied
</Files>
RedirectMatch 403 ^/includes/- 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';