
Użyj PDO z charset=utf8mb4 w DSN, ATTR_ERRMODE ustawionym na wyjątki i ATTR_EMULATE_PREPARES na false. Od PHP 8.0 błędy PDO i tak domyślnie rzucają wyjątek, a od PHP 8.1 robi to również mysqli: wzorzec if (!$conn) die(...) nie zadziała już nigdy. Każda wartość pochodząca od użytkownika idzie przez zapytanie przygotowane.
Połączenie z bazą danych to pierwsza rzecz, jaką robi większość aplikacji w PHP. Wybór rozszerzenia i sposób zapisania połączenia przesądzają o dwóch sprawach: o czytelności błędów w dniu, w którym coś się posypie, i o odporności na wstrzyknięcia SQL. Ten artykuł pokazuje połączenie tak, jak pisze się je w 2026 roku, na PHP 8.5, razem z pułapkami, które zmieniły się od PHP 8.0.
PDO, czyli połączenie do zapamiętania
PDO to ogólny interfejs PHP do baz danych. Rozmawia z MySQL-em i MariaDB, ale też z PostgreSQL-em, SQLite i SQL Serverem, przy tym samym kodzie aplikacji. To wybór domyślny, chyba że masz konkretny powód, żeby zrobić inaczej.
<?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,
]);W tym bloku liczą się trzy szczegóły, dokładnie te, o których się zapomina.
charset=utf8mb4w DSN. Bez tego połączenie używa domyślnego zestawu znaków serwera. Emoji i część znaków azjatyckich zamieniają się w znaki zapytania, a samo kodowanie staje się przy niektórych zestawach wektorem wstrzyknięcia. Zestaw znaków deklaruje się w DSN, nie zapytaniemSET NAMES.ATTR_EMULATE_PREPARESustawione nafalse. Domyślnie sterownik MySQL-a symuluje zapytania przygotowane po stronie PHP, zamiast wysyłać je na serwer. Po wyłączeniu emulacji to serwer oddziela zapytanie od danych, a wartości całkowite wracają jako liczby całkowite.- Jawne
ATTR_ERRMODE. Od PHP 8.0ERRMODE_EXCEPTIONjest już wartością domyślną. Zostawienie go w kodzie niczego nie zmienia w zachowaniu, ale mówi czytelnikowi, że błędy wracają jako wyjątki.
Sprawdzenie na maszynie testowej:
$pdo2 = new PDO($dsn, $utilisateur, $motDePasse); // żadnych opcji
var_dump($pdo2->getAttribute(PDO::ATTR_ERRMODE) === PDO::ERRMODE_EXCEPTION);
// bool(true) na PHP 8.5.10Hasła nie zapisuje się w pliku
Dane dostępowe nie mają czego szukać w wersjonowanym pliku. Najprostsza forma to odczytanie ich ze środowiska, z czytelnym komunikatem, kiedy zmiennej brakuje.
<?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,
],
);
}Operator ?: z następującym po nim throw działa od PHP 8.0, gdzie throw stało się wyrażeniem. Połączenie jest zapamiętane w zmiennej statycznej: jedno połączenie na żądanie w zupełności wystarczy.
Nigdy nie wypuszczaj komunikatu błędu na zewnątrz
Wyjątek połączenia zawiera host, użytkownika, a czasem hasło w śladzie stosu. Wyświetlony bez zmian na publicznej stronie oddaje atakującemu połowę roboty.
try {
$pdo = connexion();
} catch (PDOException $e) {
// Szczegóły trafiają do logu, nigdy do odpowiedzi HTTP
error_log('Connexion base impossible : ' . $e->getMessage());
http_response_code(503);
exit('Service momentanément indisponible.');
}Od PHP 8.2 parametry oznaczone #[SensitiveParameter] pojawiają się w śladzie stosu jako Object(SensitiveParameterValue). To przydatna ochrona, ale obejmuje wyłącznie ślad: sam komunikat pozostaje gadatliwy.
Połączenie to dopiero połowa roboty
Połączenie bez zapytań przygotowanych nie chroni przed niczym. Zasada mieści się w jednym zdaniu: danych nigdy nie skleja się z zapytaniem.
// Tak nie rób: wartość trafia wprost do zapytania
$sql = "SELECT * FROM membres WHERE email = '" . $_GET['email'] . "'";
// Zapytanie przygotowane: wartość podróżuje osobno
$requete = $pdo->prepare('SELECT id, email, cree_le FROM membres WHERE email = ?');
$requete->execute([$_GET['email'] ?? '']);
$membre = $requete->fetch();Nazwane markery czytają się lepiej, gdy wartości jest więcej niż dwie:
$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); // LIMIT zawsze z typem
$requete->execute();
foreach ($requete as $ligne) {
echo $ligne['titre'], "\n";
}PDO::PARAM_INT przy LIMIT nie jest ozdobnikiem, a jego działanie zależy od ustawienia, o którym często się zapomina. Oto co daje bindValue(':limite', 3) bez typowania, zależnie od tego, czy emulacja zapytań przygotowanych jest włączona:
| Serwer | EMULATE_PREPARES | Bez PARAM_INT | Z PARAM_INT |
|---|---|---|---|
| MariaDB 11.8.9 | false | przyjęte | przyjęte |
| MariaDB 11.8.9 | true (domyślnie) | błąd SQL 1064 | przyjęte |
| MySQL 8.4.11 | false | przyjęte | przyjęte |
| MySQL 8.4.11 | true (domyślnie) | błąd SQL 1064 | przyjęte |
Przy włączonej emulacji PDO samo składa zapytanie i otacza wartość apostrofami: LIMIT '3', czego serwer nie przyjmuje. To konfiguracja domyślna, więc przypadek, na który trafia większość ludzi. Otypowanie wartości działa we wszystkich czterech sytuacjach i tylko tę formę warto zapamiętać.
Marker nigdy nie zastąpi nazwy tabeli ani kolumny. Jeśli nazwa kolumny pochodzi od użytkownika, waliduje się ją względem zamkniętej listy:
$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";A co z mysqli?
mysqli nadal ma sens, jeśli projekt nigdy nie będzie rozmawiał z niczym poza MySQL-em i MariaDB. Jego zachowanie zmieniło się w jednym istotnym punkcie: od PHP 8.1 mysqli_report() jest ustawione na MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT, a błędy rzucają wyjątek zamiast zwracać 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();Test połączenia, który niczego już nie testuje
Poniższy wzorzec ciągnie się przez mnóstwo poradników, w tym przez wcześniejsze wersje tego. Od PHP 8.1 nie działa.
$conn = mysqli_connect($hote, $utilisateur, $motDePasse, $base);
if (!$conn) {
die('La connexion a échoué : ' . mysqli_connect_error());
}Uruchomiony na PHP 8.5.10 z błędnym hasłem, ten kod nie wypisuje przewidzianego komunikatu. Wypisuje to:
PHP Fatal error: Uncaught mysqli_sql_exception: Access denied for user
'gekkode'@'172.27.0.10' (using password: YES) in /app/test.php:8Linia if (!$conn) nigdy nie zostaje osiągnięta: mysqli_connect() rzuciło wyjątek wcześniej. die() jest martwym kodem, a surowy błąd leci do przeglądarki, jeśli wyświetlanie błędów jest włączone. Zapis z try / catch pokazany wyżej to dziś jedyna poprawna forma.
Funkcje mysql_* już nie istnieją
Spotykane jeszcze w starych skryptach mysql_connect(), mysql_query() i mysql_real_escape_string() zostały usunięte z PHP 7.0, wydanego w 2015 roku. Na PHP 8.5:
var_dump(function_exists('mysql_connect')); // bool(false)
var_dump(function_exists('mysql_real_escape_string')); // bool(false)Nie ma tu żadnej kompatybilności do utrzymania, jest tylko migracja do zrobienia, najlepiej na PDO.
Co dalej
Przy wstawianiu dużej liczby wierszy połączenie to dopiero połowa tematu: zobacz czytanie dużych plików w PHP, gdzie pokazane jest wstawianie partiami wewnątrz transakcji. Do przekierowania po zakończonej obsłudze zobacz przekierowanie w PHP. Kiedy połączenie już działa, kolejny krok to nie budować go od nowa w każdym pliku. Pozostałe poradniki PHP w hubie Programowanie webowe pokrywają sąsiednie klocki: instalacja Composera, żeby uporządkować projekt, oraz podstawy programowania obiektowego w PHP, żeby schować to połączenie w klasie zamiast w globalnej funkcji.
Częste błędy
mysqli_connect() rzuca mysqli_sql_exception, zanim wykonanie dojdzie do if (!$conn). Do przeglądarki leci surowy komunikat błędu zamiast tego, który przewidziałeś.charset=utf8mb4 emoji i część znaków zamieniają się w znaki zapytania, a kodowanie może posłużyć jako wektor wstrzyknięcia.LIMIT '3', a serwer zwraca błąd SQL 1064. Sprawdzone na MariaDB 11.8.9 i MySQL 8.4.11.

