Ligar a uma base de dados em PHP: PDO, mysqli e PHP 8.5

Ligar-se a MySQL ou MariaDB em PHP 8.5 com o PDO: DSN com charset, erros em exceções, consultas preparadas e as armadilhas do mysqli que mudaram desde o PHP 8.1.

Ligar a uma base de dados em PHP: PDO, mysqli e PHP 8.5
Resposta rápida

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.

src/Base.php
<?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=utf8mb4 no 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 consulta SET NAMES.
  • ATTR_EMULATE_PREPARES a false. 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_ERRMODE explícito. Desde o PHP 8.0, ERRMODE_EXCEPTION já é 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:

php
$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.10

A 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.

src/connexion.php
<?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.

php
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.

php
// 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:

php
$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:

php
$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
<?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.

php
$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:

code
PHP Fatal error:  Uncaught mysqli_sql_exception: Access denied for user
'gekkode'@'172.27.0.10' (using password: YES) in /app/test.php:8

A 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:

php
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

O die() que nunca é executado Desde o PHP 8.1, 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 ausente do DSN Sem 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 sem PDO::PARAM_INT Com a emulação das consultas preparadas, ativa por omissão, o PDO escreve LIMIT '3' e o servidor devolve um erro SQL 1064. Verificado em MariaDB 11.8.9 e MySQL 8.4.11.
Mensagem de exceção mostrada O rastreio de uma PDOException contém o host e o utilizador. Vai para o log, nunca para a resposta HTTP.
Marcador para um nome de coluna Um marcador só substitui um valor. Um nome de tabela ou de coluna valida-se contra uma lista fechada.

MySQLPDOPHPSécurité

Damien Flandrin Programador web desde 2010, criador da Gekkode e do Email Impact. Cada artigo é testado num projeto real antes de ser publicado. Contacto
Newsletter

Os novos testes, tutoriais e projetos, por e-mail.

Testes reproduzíveis, código versionado, resultados datados. Nunca spam.