
Usa PDO con charset=utf8mb4 en el DSN, ATTR_ERRMODE en excepción y ATTR_EMULATE_PREPARES a false. Desde PHP 8.0 los errores de PDO ya lanzan una excepción por defecto, y desde PHP 8.1 mysqli también: el patrón if (!$conn) die(...) no se dispara nunca. Todo valor que venga del usuario pasa por una consulta preparada.
Conectarse a una base de datos es lo primero que hace casi cualquier aplicación PHP. La extensión que elijas y la forma de escribir la conexión deciden dos cosas: si los errores se entienden el día que algo se rompe, y la resistencia a las inyecciones SQL. Este artículo muestra la conexión tal y como se escribe en 2026, sobre PHP 8.5, con las trampas que han cambiado desde PHP 8.0.
PDO, la conexión que debes elegir
PDO es la interfaz genérica de PHP para las bases de datos. Habla con MySQL y MariaDB, pero también con PostgreSQL, SQLite o SQL Server, con el mismo código de aplicación. Es la opción por defecto, salvo que tengas una razón concreta para hacer otra cosa.
<?php
declare(strict_types=1);
$dsn = 'mysql:host=127.0.0.1;port=3306;dbname=ma_base;charset=utf8mb4';
$pdo = new PDO($dsn, 'utilisateur', 'mot_de_passe', [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false,
]);En este bloque cuentan tres detalles, y son justo los que se olvidan.
charset=utf8mb4en el DSN. Sin él, la conexión usa el juego de caracteres por defecto del servidor. Los emoji y parte de los caracteres asiáticos se convierten en signos de interrogación, y la codificación pasa a ser un vector de inyección con ciertos juegos de caracteres. El charset se declara en el DSN, no con una consultaSET NAMES.ATTR_EMULATE_PREPARESafalse. Por defecto, el driver de MySQL simula las consultas preparadas del lado de PHP en lugar de enviarlas al servidor. Al desactivarlo, es el servidor el que separa la consulta de los datos, y los tipos enteros siguen siendo enteros a la vuelta.ATTR_ERRMODEexplícito. Desde PHP 8.0,ERRMODE_EXCEPTIONya es el valor por defecto. Dejarlo escrito no cambia el comportamiento, pero le dice a quien lee el código que los errores salen como excepciones.
Comprobación en la máquina de pruebas:
$pdo2 = new PDO($dsn, $utilisateur, $motDePasse); // sin opciones
var_dump($pdo2->getAttribute(PDO::ATTR_ERRMODE) === PDO::ERRMODE_EXCEPTION);
// bool(true) en PHP 8.5.10La contraseña no se escribe en el archivo
Las credenciales no pintan nada en un archivo versionado. La forma más simple es leerlas del entorno, con un mensaje claro si falta la variable.
<?php
declare(strict_types=1);
function connexion(): PDO
{
static $pdo = null;
if ($pdo instanceof PDO) {
return $pdo;
}
$hote = getenv('DB_HOST') ?: '127.0.0.1';
$base = getenv('DB_NAME') ?: throw new RuntimeException('DB_NAME manquant');
$user = getenv('DB_USER') ?: throw new RuntimeException('DB_USER manquant');
$pass = getenv('DB_PASS') ?: '';
return $pdo = new PDO(
"mysql:host={$hote};dbname={$base};charset=utf8mb4",
$user,
$pass,
[
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false,
],
);
}El operador ?: seguido de throw funciona desde PHP 8.0, cuando throw pasó a ser una expresión. La conexión se guarda en una variable estática: abrir una sola conexión por petición es más que suficiente.
Nunca dejes escapar el mensaje de error
Una excepción de conexión contiene el host, el usuario y, a veces, la contraseña dentro de la traza. Mostrada tal cual en una página pública, le regala medio trabajo a un atacante.
try {
$pdo = connexion();
} catch (PDOException $e) {
// El detalle va al registro, nunca a la respuesta HTTP
error_log('Connexion base impossible : ' . $e->getMessage());
http_response_code(503);
exit('Service momentanément indisponible.');
}Desde PHP 8.2, los parámetros marcados con #[SensitiveParameter] aparecen como Object(SensitiveParameterValue) en las trazas. Es una protección útil, pero solo cubre la traza: el mensaje sigue siendo igual de hablador.
Conectarse es solo la mitad del trabajo
Una conexión sin consultas preparadas no protege de nada. La regla cabe en una frase: los datos nunca se concatenan dentro de la consulta.
// No hagas esto: el valor entra directamente en la consulta
$sql = "SELECT * FROM membres WHERE email = '" . $_GET['email'] . "'";
// Consulta preparada: el valor viaja aparte
$requete = $pdo->prepare('SELECT id, email, cree_le FROM membres WHERE email = ?');
$requete->execute([$_GET['email'] ?? '']);
$membre = $requete->fetch();Los marcadores con nombre se leen mejor en cuanto hay más de dos valores:
$requete = $pdo->prepare(
'SELECT id, titre FROM articles
WHERE statut = :statut AND publie_le >= :depuis
ORDER BY publie_le DESC LIMIT :limite'
);
$requete->bindValue(':statut', 'publie');
$requete->bindValue(':depuis', '2026-01-01');
$requete->bindValue(':limite', 10, PDO::PARAM_INT); // tipar siempre el LIMIT
$requete->execute();
foreach ($requete as $ligne) {
echo $ligne['titre'], "\n";
}El PDO::PARAM_INT del LIMIT no es decorativo, y su efecto depende de un ajuste que se olvida a menudo. Esto es lo que da bindValue(':limite', 3) sin tipar, según si la emulación de consultas preparadas está activa o no:
| Servidor | EMULATE_PREPARES | Sin PARAM_INT | Con PARAM_INT |
|---|---|---|---|
| MariaDB 11.8.9 | false | aceptado | aceptado |
| MariaDB 11.8.9 | true (por defecto) | error SQL 1064 | aceptado |
| MySQL 8.4.11 | false | aceptado | aceptado |
| MySQL 8.4.11 | true (por defecto) | error SQL 1064 | aceptado |
Con la emulación, PDO construye la consulta él mismo y rodea el valor de comillas: LIMIT '3', que el servidor rechaza. Es la configuración por defecto, así que es el caso con el que se topa la mayoría. Tipar el valor funciona en las cuatro situaciones: es la única forma que hay que recordar.
Un marcador nunca sustituye a un nombre de tabla o de columna. Si el nombre de columna viene del usuario, se valida contra una lista cerrada:
$colonnesTriables = ['titre', 'publie_le', 'vues'];
$tri = in_array($_GET['tri'] ?? '', $colonnesTriables, true) ? $_GET['tri'] : 'publie_le';
$sql = "SELECT id, titre FROM articles ORDER BY {$tri} DESC";¿Y mysqli?
mysqli sigue siendo válido si el proyecto nunca va a hablar con nada que no sea MySQL o MariaDB. Su comportamiento ha cambiado en un punto importante: desde PHP 8.1, mysqli_report() está ajustado a MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT, y los errores lanzan una excepción en lugar de devolver false.
<?php
declare(strict_types=1);
try {
$mysqli = new mysqli('127.0.0.1', $utilisateur, $motDePasse, 'ma_base');
$mysqli->set_charset('utf8mb4');
} catch (mysqli_sql_exception $e) {
error_log('Connexion mysqli impossible : ' . $e->getMessage());
http_response_code(503);
exit('Service momentanément indisponible.');
}
$requete = $mysqli->prepare('SELECT id, email FROM membres WHERE email = ?');
$requete->bind_param('s', $email);
$requete->execute();
$membre = $requete->get_result()->fetch_assoc();La prueba de conexión que ya no prueba nada
El patrón siguiente arrastra por muchísimos tutoriales, incluidas las versiones anteriores de este. Ya no funciona desde PHP 8.1.
$conn = mysqli_connect($hote, $utilisateur, $motDePasse, $base);
if (!$conn) {
die('La connexion a échoué : ' . mysqli_connect_error());
}Ejecutado en PHP 8.5.10 con una contraseña incorrecta, este código no produce el mensaje previsto. Produce esto:
PHP Fatal error: Uncaught mysqli_sql_exception: Access denied for user
'gekkode'@'172.27.0.10' (using password: YES) in /app/test.php:8La línea if (!$conn) no se alcanza nunca: mysqli_connect() ha lanzado la excepción antes. El die() es código muerto, y el error en bruto sale hacia el navegador si la visualización de errores está activa. La reescritura con try / catch de más arriba es la única forma correcta hoy.
Las funciones mysql_* ya no existen
Los mysql_connect(), mysql_query() y mysql_real_escape_string() que todavía se cruzan en scripts antiguos se retiraron en PHP 7.0, publicado en 2015. En PHP 8.5:
var_dump(function_exists('mysql_connect')); // bool(false)
var_dump(function_exists('mysql_real_escape_string')); // bool(false)No hay compatibilidad que mantener, solo una migración que hacer, preferiblemente hacia PDO.
Para ir más lejos
Para insertar un gran volumen de filas, la conexión es solo la mitad del asunto: consulta leer archivos grandes con PHP, que muestra la inserción por lotes dentro de una transacción. Para redirigir después de un tratamiento, consulta crear una redirección en PHP. Una vez montada la conexión, lo siguiente es no rehacerla en cada archivo. Los demás tutoriales de PHP del hub de Desarrollo web cubren las piezas vecinas: instalar Composer para organizar el proyecto, y las bases de la POO en PHP para guardar esta conexión en una clase en lugar de en una función global.
Errores frecuentes
mysqli_connect() lanza una mysqli_sql_exception antes de llegar a if (!$conn). El mensaje de error en bruto sale hacia el navegador en lugar del mensaje previsto.charset=utf8mb4, los emoji y ciertos caracteres se convierten en signos de interrogación, y la codificación puede servir de vector de inyección.LIMIT '3' y el servidor devuelve un error SQL 1064. Comprobado en MariaDB 11.8.9 y MySQL 8.4.11.

