
Usa o PDO com charset=utf8mb4 no DSN, ATTR_ERRMODE em exceção e ATTR_EMULATE_PREPARES a false. Desde o PHP 8.0 os erros do PDO já lançam uma exceção por omissão, e desde o PHP 8.1 o mysqli também: o padrão if (!$conn) die(...) nunca mais dispara. Qualquer valor vindo do utilizador passa por uma consulta preparada.
Ligar-se a uma base de dados é a primeira coisa que a maior parte das aplicações PHP faz. A escolha da extensão e a forma de escrever a ligação decidem duas coisas: a legibilidade dos erros no dia em que algo parte, e a resistência às injeções SQL. Este artigo mostra a ligação tal como se escreve em 2026, em PHP 8.5, com as armadilhas que mudaram desde o PHP 8.0.
O PDO, a ligação a reter
O PDO é a interface genérica do PHP para as bases de dados. Fala com MySQL e MariaDB, mas também com PostgreSQL, SQLite ou SQL Server, com o mesmo código aplicacional. É a escolha por omissão, salvo razão precisa para fazer de outra forma.
<?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,
]);Há três detalhes que contam neste bloco, e são exatamente os que se esquecem.
charset=utf8mb4no DSN. Sem ele, a ligação usa o conjunto de caracteres por omissão do servidor. Os emoji e uma parte dos caracteres asiáticos passam a pontos de interrogação, e a codificação torna-se um vetor de injeção em certos conjuntos de caracteres. O charset declara-se no DSN, não com uma consultaSET NAMES.ATTR_EMULATE_PREPARESafalse. Por omissão, o driver MySQL simula as consultas preparadas do lado do PHP em vez de as enviar ao servidor. Ao desativar a emulação, é o servidor que separa a consulta dos dados, e os inteiros voltam como inteiros.ATTR_ERRMODEexplícito. Desde o PHP 8.0,ERRMODE_EXCEPTIONjá é o valor por omissão. Deixá-lo escrito não muda nada ao comportamento, mas diz a quem lê o código que os erros sobem em exceções.
Verificação na máquina de teste:
$pdo2 = new PDO($dsn, $utilisateur, $motDePasse); // sem nenhuma opção
var_dump($pdo2->getAttribute(PDO::ATTR_ERRMODE) === PDO::ERRMODE_EXCEPTION);
// bool(true) em PHP 8.5.10A palavra-passe não se escreve no ficheiro
As credenciais não têm nada que fazer num ficheiro versionado. A forma mais simples é lê-las no ambiente, com uma mensagem clara quando a variável falta.
<?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,
],
);
}O operador ?: seguido de throw funciona desde o PHP 8.0, versão em que throw passou a ser uma expressão. A ligação fica guardada numa variável estática: abrir uma ligação por pedido chega e sobra.
Nunca deixar fugir a mensagem de erro
Uma exceção de ligação contém o host, o utilizador e por vezes a palavra-passe no rastreio. Mostrada tal e qual numa página pública, oferece metade do trabalho a um atacante.
try {
$pdo = connexion();
} catch (PDOException $e) {
// O detalhe vai para o log, nunca para a resposta HTTP
error_log('Connexion base impossible : ' . $e->getMessage());
http_response_code(503);
exit('Service momentanément indisponible.');
}Desde o PHP 8.2, os parâmetros marcados com #[SensitiveParameter] aparecem como Object(SensitiveParameterValue) nos rastreios. É uma proteção útil, mas só cobre o rastreio: a mensagem continua faladora.
Ligar-se é só metade do trabalho
Uma ligação sem consultas preparadas não protege de nada. A regra cabe numa frase: os dados nunca se concatenam na consulta.
// A não fazer: o valor entra diretamente na consulta
$sql = "SELECT * FROM membres WHERE email = '" . $_GET['email'] . "'";
// Consulta preparada: o valor viaja à parte
$requete = $pdo->prepare('SELECT id, email, cree_le FROM membres WHERE email = ?');
$requete->execute([$_GET['email'] ?? '']);
$membre = $requete->fetch();Os marcadores nomeados leem-se melhor assim que há mais de dois 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 sempre o LIMIT
$requete->execute();
foreach ($requete as $ligne) {
echo $ligne['titre'], "\n";
}O PDO::PARAM_INT no LIMIT não é decorativo, e o seu efeito depende de uma definição que se esquece com frequência. Eis o que dá bindValue(':limite', 3) sem tipagem, consoante a emulação das consultas preparadas esteja ativa ou não:
| Servidor | EMULATE_PREPARES | Sem PARAM_INT | Com PARAM_INT |
|---|---|---|---|
| MariaDB 11.8.9 | false | aceite | aceite |
| MariaDB 11.8.9 | true (por omissão) | erro SQL 1064 | aceite |
| MySQL 8.4.11 | false | aceite | aceite |
| MySQL 8.4.11 | true (por omissão) | erro SQL 1064 | aceite |
Com a emulação, é o PDO que fabrica a consulta e envolve o valor em aspas: LIMIT '3', que o servidor recusa. É a configuração por omissão, portanto o caso com que a maioria das pessoas esbarra. Tipar o valor funciona nas quatro situações, é a única forma a reter.
Um marcador nunca substitui um nome de tabela ou de coluna. Se o nome da coluna vier do utilizador, valida-se contra uma lista fechada:
$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 o mysqli?
O mysqli continua válido se o projeto nunca vier a falar com outra coisa senão MySQL ou MariaDB. O seu comportamento mudou num ponto importante: desde o PHP 8.1, mysqli_report() está definido como MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT, e os erros lançam uma exceção em vez de devolverem 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();O teste de ligação que já não testa nada
O padrão seguinte arrasta-se por imensos tutoriais, incluindo as versões anteriores deste. Deixou de funcionar no PHP 8.1.
$conn = mysqli_connect($hote, $utilisateur, $motDePasse, $base);
if (!$conn) {
die('La connexion a échoué : ' . mysqli_connect_error());
}Executado em PHP 8.5.10 com uma palavra-passe errada, este código não produz a mensagem prevista. Produz isto:
PHP Fatal error: Uncaught mysqli_sql_exception: Access denied for user
'gekkode'@'172.27.0.10' (using password: YES) in /app/test.php:8A linha if (!$conn) nunca é atingida: mysqli_connect() lançou a exceção antes. O die() é código morto, e o erro em bruto parte para o browser se a apresentação dos erros estiver ativa. A reescrita com try / catch mais acima é hoje a única forma correta.
As funções mysql_* já não existem
Os mysql_connect(), mysql_query() e mysql_real_escape_string() que ainda se encontram em scripts antigos foram retirados do PHP 7.0, lançado em 2015. Em PHP 8.5:
var_dump(function_exists('mysql_connect')); // bool(false)
var_dump(function_exists('mysql_real_escape_string')); // bool(false)Não há compatibilidade a assegurar, apenas uma migração a fazer, de preferência para o PDO.
Para ir mais longe
Para inserir um grande volume de linhas, a ligação é apenas metade do assunto: vê ler ficheiros grandes em PHP, que mostra a inserção por lotes dentro de uma transação. Para redirecionar depois de um tratamento, vê criar um redirecionamento em PHP. Uma vez a ligação instalada, a etapa seguinte é não a voltar a escrever em cada ficheiro. Os outros tutoriais de PHP do hub Desenvolvimento web cobrem as peças vizinhas: instalar o Composer para organizar o projeto, e as bases da POO em PHP para arrumar esta ligação numa classe em vez de numa função global.
Erros frequentes
mysqli_connect() lança uma mysqli_sql_exception antes de chegar ao if (!$conn). A mensagem de erro em bruto parte para o browser em vez da mensagem prevista.charset=utf8mb4, os emoji e alguns caracteres passam a pontos de interrogação, e a codificação pode servir de vetor de injeção.LIMIT '3' e o servidor devolve um erro SQL 1064. Verificado em MariaDB 11.8.9 e MySQL 8.4.11.

