Datenbankverbindung in PHP: PDO, mysqli und Prepared Statements

Als Webentwickler musst du dich ständig mit einer Datenbank verbinden, um Daten zu speichern und wieder zu lesen. Dieser Artikel zeigt Schritt für Schritt, wie du diese Verbindung in PHP aufbaust.

Datenbankverbindung in PHP: PDO, mysqli und Prepared Statements
Schnelle Antwort

Nimm PDO mit charset=utf8mb4 im DSN, ATTR_ERRMODE auf Exception und ATTR_EMULATE_PREPARES auf false. Seit PHP 8.0 werfen PDO-Fehler ohnehin eine Exception, seit PHP 8.1 auch mysqli: Das Muster if (!$conn) die(...) greift nie mehr. Jeder Wert, der vom Benutzer kommt, läuft über ein Prepared Statement.

Die Verbindung zur Datenbank ist das Erste, was die meisten PHP-Anwendungen tun. Welche Extension du wählst und wie du die Verbindung schreibst, entscheidet über zwei Dinge: wie lesbar die Fehler an dem Tag sind, an dem etwas kaputtgeht, und wie widerstandsfähig der Code gegen SQL-Injections ist. Dieser Artikel zeigt die Verbindung so, wie man sie 2026 unter PHP 8.5 schreibt, samt der Fallen, die sich seit PHP 8.0 verändert haben.

PDO ist die Verbindung der Wahl

PDO ist die generische Datenbankschnittstelle von PHP. Sie spricht mit MySQL und MariaDB, aber ebenso mit PostgreSQL, SQLite oder SQL Server, und zwar mit demselben Anwendungscode. Das ist die Standardwahl, solange kein konkreter Grund dagegen spricht.

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,
]);

Drei Details in diesem Block sind entscheidend, und genau die werden vergessen.

  • charset=utf8mb4 im DSN. Fehlt es, arbeitet die Verbindung mit dem Standardzeichensatz des Servers. Emojis und ein Teil der asiatischen Zeichen werden zu Fragezeichen, und bei manchen Zeichensätzen wird die Kodierung selbst zum Vektor für eine Injection. Der Zeichensatz gehört in das DSN, nicht in eine Abfrage SET NAMES.
  • ATTR_EMULATE_PREPARES auf false. Standardmäßig simuliert der MySQL-Treiber die Prepared Statements auf PHP-Seite, statt sie an den Server zu schicken. Schaltest du die Emulation ab, trennt der Server die Abfrage von den Daten, und Integer-Werte kommen als Integer zurück.
  • ATTR_ERRMODE ausdrücklich setzen. Seit PHP 8.0 ist ERRMODE_EXCEPTION ohnehin der Standardwert. Die Zeile ändert nichts am Verhalten, sagt dem Leser des Codes aber, dass Fehler als Exceptions hochkommen.

Gegenprobe auf der Testmaschine:

php
$pdo2 = new PDO($dsn, $utilisateur, $motDePasse); // keine Optionen
var_dump($pdo2->getAttribute(PDO::ATTR_ERRMODE) === PDO::ERRMODE_EXCEPTION);
// bool(true) unter PHP 8.5.10

Das Passwort gehört nicht in die Datei

Zugangsdaten haben in einer versionierten Datei nichts verloren. Am einfachsten liest du sie aus der Umgebung, mit einer klaren Meldung, falls eine Variable fehlt.

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,
        ],
    );
}

Der Operator ?: gefolgt von throw funktioniert seit PHP 8.0, seit throw ein Ausdruck ist. Die Verbindung liegt in einer statischen Variablen: eine Verbindung pro Request reicht vollkommen aus.

Die Fehlermeldung darf niemals nach außen

Eine Verbindungs-Exception enthält den Host, den Benutzer und im Stacktrace manchmal auch das Passwort. Ungefiltert auf einer öffentlichen Seite ausgegeben, schenkt sie einem Angreifer die halbe Arbeit.

php
try {
    $pdo = connexion();
} catch (PDOException $e) {
    // Das Detail geht ins Log, niemals in die HTTP-Antwort
    error_log('Connexion base impossible : ' . $e->getMessage());
    http_response_code(503);
    exit('Service momentanément indisponible.');
}

Seit PHP 8.2 erscheinen Parameter mit #[SensitiveParameter] im Stacktrace als Object(SensitiveParameterValue). Der Schutz ist nützlich, deckt aber nur den Stacktrace ab: die Meldung selbst bleibt geschwätzig.

Die Verbindung ist nur die halbe Arbeit

Eine Verbindung ohne Prepared Statement schützt vor gar nichts. Die Regel passt in einen Satz: Daten werden niemals in die Abfrage hineingeschrieben.

php
// So nicht: der Wert landet direkt in der Abfrage
$sql = "SELECT * FROM membres WHERE email = '" . $_GET['email'] . "'";

// Prepared Statement: der Wert reist getrennt
$requete = $pdo->prepare('SELECT id, email, cree_le FROM membres WHERE email = ?');
$requete->execute([$_GET['email'] ?? '']);
$membre = $requete->fetch();

Benannte Platzhalter lesen sich besser, sobald mehr als zwei Werte im Spiel sind:

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); // das LIMIT immer typisieren
$requete->execute();

foreach ($requete as $ligne) {
    echo $ligne['titre'], "\n";
}

Das PDO::PARAM_INT am LIMIT ist keine Zierde, und seine Wirkung hängt an einer Einstellung, die gern übersehen wird. So verhält sich bindValue(':limite', 3) ohne Typangabe, je nachdem ob die Emulation der Prepared Statements aktiv ist oder nicht:

Server EMULATE_PREPARES Ohne PARAM_INT Mit PARAM_INT
MariaDB 11.8.9 false akzeptiert akzeptiert
MariaDB 11.8.9 true (Standard) SQL-Fehler 1064 akzeptiert
MySQL 8.4.11 false akzeptiert akzeptiert
MySQL 8.4.11 true (Standard) SQL-Fehler 1064 akzeptiert

Mit Emulation baut PDO die Abfrage selbst zusammen und setzt den Wert in Anführungszeichen: LIMIT '3', was der Server ablehnt. Das ist die Standardkonfiguration, also der Fall, den die meisten antreffen. Den Wert zu typisieren funktioniert in allen vier Situationen, und nur diese Form solltest du dir merken.

Ein Platzhalter ersetzt nie einen Tabellen- oder Spaltennamen. Kommt der Spaltenname vom Benutzer, prüfst du ihn gegen eine geschlossene Liste:

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";

Und mysqli?

mysqli bleibt eine gültige Wahl, wenn das Projekt nie mit etwas anderem als MySQL oder MariaDB sprechen wird. In einem wichtigen Punkt hat sich das Verhalten geändert: Seit PHP 8.1 steht mysqli_report() auf MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT, und Fehler werfen eine Exception, statt false zurückzugeben.

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();

Der Verbindungstest, der nichts mehr testet

Das folgende Muster geistert durch unzählige Tutorials, auch durch die früheren Fassungen dieses hier. Seit PHP 8.1 funktioniert es nicht mehr.

php
$conn = mysqli_connect($hote, $utilisateur, $motDePasse, $base);

if (!$conn) {
    die('La connexion a échoué : ' . mysqli_connect_error());
}

Unter PHP 8.5.10 mit einem falschen Passwort ausgeführt, gibt dieser Code nicht die vorgesehene Meldung aus. Er gibt das hier aus:

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

Die Zeile if (!$conn) wird nie erreicht: mysqli_connect() hat die Exception vorher geworfen. Das die() ist toter Code, und der rohe Fehler geht an den Browser, sobald die Fehleranzeige aktiv ist. Die Fassung mit try / catch weiter oben ist heute die einzig richtige.

Die Funktionen mysql_* gibt es nicht mehr

mysql_connect(), mysql_query() und mysql_real_escape_string(), die einem in alten Skripten noch begegnen, wurden mit PHP 7.0 entfernt, erschienen 2015. Unter PHP 8.5:

php
var_dump(function_exists('mysql_connect'));           // bool(false)
var_dump(function_exists('mysql_real_escape_string')); // bool(false)

Es gibt hier keine Kompatibilität zu wahren, nur eine Migration zu erledigen, am besten nach PDO.

Zum Weiterlesen

Beim Einfügen großer Zeilenmengen ist die Verbindung nur die halbe Geschichte: siehe große Dateien mit PHP lesen, wo das stapelweise Einfügen in einer Transaktion gezeigt wird. Für eine Weiterleitung nach der Verarbeitung siehe eine Weiterleitung in PHP erstellen. Steht die Verbindung erst einmal, geht es darum, sie nicht in jeder Datei neu aufzubauen. Die übrigen PHP-Tutorials im Hub Webentwicklung decken die benachbarten Bausteine ab: Composer installieren, um das Projekt zu strukturieren, und die Grundlagen der OOP in PHP, um diese Verbindung in einer Klasse unterzubringen statt in einer globalen Funktion.

Häufige Fehler

Das die(), das nie ausgeführt wird Seit PHP 8.1 wirft mysqli_connect() eine mysqli_sql_exception, bevor if (!$conn) überhaupt erreicht wird. Statt der vorgesehenen Meldung geht der rohe Fehlertext an den Browser.
Zeichensatz fehlt im DSN Ohne charset=utf8mb4 werden Emojis und manche Zeichen zu Fragezeichen, und die Kodierung kann als Vektor für eine Injection dienen.
LIMIT ohne PDO::PARAM_INT Mit der standardmäßig aktiven Emulation der Prepared Statements schreibt PDO LIMIT '3', und der Server antwortet mit dem SQL-Fehler 1064. Geprüft auf MariaDB 11.8.9 und MySQL 8.4.11.
Exception-Meldung angezeigt Der Stacktrace einer PDOException enthält Host und Benutzer. Er gehört ins Log, niemals in die HTTP-Antwort.
Platzhalter für einen Spaltennamen Ein Platzhalter ersetzt nur einen Wert. Ein Tabellen- oder Spaltenname wird gegen eine geschlossene Liste geprüft.

MySQLPDOPHPSécurité

Damien Flandrin Webentwickler seit 2010, Gründer von Gekkode und Email Impact. Jeder Artikel wird vor der Veröffentlichung an einem echten Projekt getestet. Kontakt
Newsletter

Neue Tests, Tutorials und Projekte, per E-Mail.

Reproduzierbare Tests, versionierter Code, datierte Ergebnisse. Niemals Spam.