
Usa PDO con charset=utf8mb4 nel DSN, ATTR_ERRMODE su exception e ATTR_EMULATE_PREPARES a false. Da PHP 8.0 gli errori PDO lanciano già un'eccezione per impostazione predefinita, e da PHP 8.1 lo fa anche mysqli: il pattern if (!$conn) die(...) non scatta più. Ogni valore che arriva dall'utente passa da una query preparata.
Connettersi a un database è la prima cosa che fa quasi ogni applicazione PHP. La scelta dell’estensione e il modo in cui scrivi la connessione decidono due cose: quanto saranno leggibili gli errori il giorno in cui qualcosa si rompe, e quanto il codice resiste alle SQL injection. Qui vediamo la connessione come la si scrive nel 2026, su PHP 8.5, con le trappole che sono cambiate a partire da PHP 8.0.
PDO, la connessione da ricordare
PDO è l’interfaccia generica di PHP per i database. Parla con MySQL e MariaDB, ma anche con PostgreSQL, SQLite o SQL Server, a parità di codice applicativo. È la scelta predefinita, salvo motivi precisi per fare altrimenti.
<?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,
]);In questo blocco contano tre dettagli, e sono esattamente quelli che si dimenticano.
charset=utf8mb4nel DSN. Senza, la connessione usa il set di caratteri predefinito del server. Le emoji e una parte dei caratteri asiatici diventano punti interrogativi, e su certi set di caratteri la codifica si trasforma in un vettore di injection. Il charset si dichiara nel DSN, non con una querySET NAMES.ATTR_EMULATE_PREPARESafalse. Per impostazione predefinita il driver MySQL simula le query preparate lato PHP invece di inviarle al server. Disattivando l’emulazione è il server a separare la query dai dati, e i tipi interi tornano indietro come interi.ATTR_ERRMODEesplicito. Da PHP 8.0ERRMODE_EXCEPTIONè già il valore predefinito. Lasciarlo scritto non cambia il comportamento, ma dice a chi legge il codice che gli errori risalgono come eccezioni.
Verifica sulla macchina di test:
$pdo2 = new PDO($dsn, $utilisateur, $motDePasse); // nessuna opzione
var_dump($pdo2->getAttribute(PDO::ATTR_ERRMODE) === PDO::ERRMODE_EXCEPTION);
// bool(true) su PHP 8.5.10La password non si scrive nel file
Le credenziali non hanno niente da fare in un file versionato. La forma più semplice è leggerle dall’ambiente, con un messaggio chiaro se la variabile manca.
<?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,
],
);
}L’operatore ?: seguito da throw funziona da PHP 8.0, da quando throw è diventato un’espressione. La connessione è memorizzata in una variabile statica: aprire una connessione per richiesta è più che sufficiente.
Il messaggio di errore non deve mai trapelare
Un’eccezione di connessione contiene l’host, l’utente e a volte la password nella traccia. Mostrata così com’è su una pagina pubblica, regala metà del lavoro a chi attacca.
try {
$pdo = connexion();
} catch (PDOException $e) {
// Il dettaglio va nel log, mai nella risposta HTTP
error_log('Connexion base impossible : ' . $e->getMessage());
http_response_code(503);
exit('Service momentanément indisponible.');
}Da PHP 8.2 i parametri contrassegnati con #[SensitiveParameter] compaiono come Object(SensitiveParameterValue) nelle tracce. È una protezione utile, ma copre solo la traccia: il messaggio, invece, resta chiacchierone.
Connettersi è solo metà del lavoro
Una connessione senza query preparate non protegge da niente. La regola sta in una frase: i dati non si concatenano mai dentro la query.
// Da non fare: il valore entra direttamente nella query
$sql = "SELECT * FROM membres WHERE email = '" . $_GET['email'] . "'";
// Query preparata: il valore viaggia a parte
$requete = $pdo->prepare('SELECT id, email, cree_le FROM membres WHERE email = ?');
$requete->execute([$_GET['email'] ?? '']);
$membre = $requete->fetch();I segnaposto con nome si leggono meglio non appena i valori sono più di due:
$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); // tipizza sempre il LIMIT
$requete->execute();
foreach ($requete as $ligne) {
echo $ligne['titre'], "\n";
}Il PDO::PARAM_INT sul LIMIT non è decorativo, e il suo effetto dipende da un’impostazione che si tende a dimenticare. Ecco cosa succede con bindValue(':limite', 3) senza tipo, a seconda che l’emulazione delle query preparate sia attiva o no:
| Server | EMULATE_PREPARES | Senza PARAM_INT | Con PARAM_INT |
|---|---|---|---|
| MariaDB 11.8.9 | false | accettato | accettato |
| MariaDB 11.8.9 | true (predefinito) | errore SQL 1064 | accettato |
| MySQL 8.4.11 | false | accettato | accettato |
| MySQL 8.4.11 | true (predefinito) | errore SQL 1064 | accettato |
Con l’emulazione è PDO a costruire la query, e mette il valore fra apici: LIMIT '3', che il server rifiuta. È la configurazione predefinita, quindi il caso in cui finisce la maggior parte delle persone. Tipizzare il valore funziona in tutte e quattro le situazioni: è l’unica forma da ricordare.
Un segnaposto non sostituisce mai il nome di una tabella o di una colonna. Se il nome della colonna arriva dall’utente, va validato contro un elenco chiuso:
$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";E mysqli?
mysqli resta valido se il progetto non parlerà mai con altro che MySQL o MariaDB. Il suo comportamento è cambiato su un punto importante: da PHP 8.1 mysqli_report() è impostato su MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT, e gli errori lanciano un’eccezione invece di restituire 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();Il test di connessione che non testa più niente
Il pattern che segue si trascina in moltissimi tutorial, comprese le versioni precedenti di questo. Non funziona più da PHP 8.1.
$conn = mysqli_connect($hote, $utilisateur, $motDePasse, $base);
if (!$conn) {
die('La connexion a échoué : ' . mysqli_connect_error());
}Eseguito su PHP 8.5.10 con una password sbagliata, questo codice non produce il messaggio previsto. Produce questo:
PHP Fatal error: Uncaught mysqli_sql_exception: Access denied for user
'gekkode'@'172.27.0.10' (using password: YES) in /app/test.php:8La riga if (!$conn) non viene mai raggiunta: mysqli_connect() ha già lanciato l’eccezione. Il die() è codice morto, e l’errore grezzo parte verso il browser se la visualizzazione degli errori è attiva. La riscrittura con try / catch vista sopra è oggi l’unica forma corretta.
Le funzioni mysql_* non esistono più
I mysql_connect(), mysql_query() e mysql_real_escape_string() che si incontrano ancora in vecchi script sono stati rimossi da PHP 7.0, uscito nel 2015. Su PHP 8.5:
var_dump(function_exists('mysql_connect')); // bool(false)
var_dump(function_exists('mysql_real_escape_string')); // bool(false)Non c’è nessuna compatibilità da garantire, solo una migrazione da fare, preferibilmente verso PDO.
Per approfondire
Per inserire un grosso volume di righe la connessione è solo metà del discorso: vedi leggere file di grandi dimensioni in PHP, che mostra l’inserimento a lotti dentro una transazione. Per reindirizzare al termine di un’elaborazione, vedi creare un redirect in PHP. Una volta messa in piedi la connessione, il passo successivo è non riscriverla in ogni file. Gli altri tutorial PHP dell’hub Sviluppo web coprono i mattoni vicini: installare Composer per organizzare il progetto, e le basi della programmazione a oggetti in PHP per sistemare questa connessione in una classe invece che in una funzione globale.
Errori frequenti
mysqli_connect() lancia una mysqli_sql_exception prima di arrivare a if (!$conn). Il messaggio di errore grezzo parte verso il browser al posto del messaggio previsto.charset=utf8mb4 le emoji e certi caratteri diventano punti interrogativi, e la codifica può fare da vettore di injection.LIMIT '3' e il server restituisce un errore SQL 1064. Verificato su MariaDB 11.8.9 e MySQL 8.4.11.

