PHP MySQL Prepared Statements: Ein umfassender Leitfaden
Prepared Statements steigern Sicherheit und Effizienz von PHP-Anwendungen mit Datenbankzugriff. Dieser Leitfaden erklärt MySQLi und PDO im Detail.
Prepared Statements sind die wichtigste Technik für das Schreiben von sicherem PHP-Code, der mit einer Datenbank kommuniziert. Sie trennen den SQL-Befehl von den Daten, auf die er zugreift – das verhindert SQL-Injection zuverlässig und beschleunigt außerdem Abfragen, die wiederholt ausgeführt werden. Dieser Leitfaden erklärt, was Prepared Statements sind, warum sie wichtig sind und wie man sie sowohl mit der MySQLi- als auch mit der PDO-Erweiterung einsetzt.
Diese Seite behandelt:
- Was ein Prepared Statement ist und warum das Modell „einmal kompilieren, viele Male ausführen" existiert
- Vorbereitung von
INSERT- undSELECT-Abfragen mit MySQLi - Dieselben Muster mit PDO (benannte Platzhalter)
- Häufige Fallstricke: Fehlerreporting, falscher Bindungstyp und Wiederverwendung von Statements
Was sind Prepared Statements?
Ein Prepared Statement ist eine SQL-Abfrage, die in zwei Stufen an die Datenbank gesendet wird:
- Prepare — Sie senden das SQL mit
?-(oder:name-) Platzhaltern anstelle echter Werte. Die Datenbank parst, kompiliert und optimiert diese Vorlage einmalig. - Execute — Sie senden die eigentlichen Werte separat. Die Datenbank fügt sie in den bereits kompilierten Plan ein und führt ihn aus.
Da die Werte über einen anderen Kanal als der SQL-Text übertragen werden, verwechselt die Datenbank Daten niemals mit Befehlen. Ein Wert wie ' OR '1'='1 wird als literaler Suchstring behandelt, nicht als auszuführendes SQL – genau deshalb scheitern Injection-Angriffe an Prepared Statements.
Warum Prepared Statements verwenden?
- Sicherheit. Benutzereingaben können die Struktur einer Abfrage niemals verändern. Das ist der empfohlene Schutz gegen SQL-Injection und der Grund, warum Abfragen niemals durch Verkettung von Variablen zu einem String gebaut werden sollten.
- Performance. Die Abfrage wird einmal geparst und kompiliert. Wenn Sie sie viele Male ausführen (z. B. 1.000 Zeilen in einer Schleife einfügen), verwendet die Datenbank denselben Plan wieder, anstatt ihn jedes Mal neu zu parsen.
- Saubererer Code. Platzhalter bedeuten kein manuelles Escaping mit
mysqli_real_escape_string()und kein Jonglieren mit Anführungszeichen. Sie binden eine Variable – fertig.
Faustregel: Sobald ein Teil einer Abfrage aus Benutzereingaben stammt – ein Formularfeld, ein URL-Parameter, ein Cookie – verwenden Sie ein Prepared Statement.
Die Schritte
Jedes Prepared Statement folgt demselben Lebenszyklus:
- Verbindung zur Datenbank herstellen.
- Das SQL mit Platzhaltern vorbereiten.
- Variablen an die Platzhalter binden.
- Das Statement ausführen.
- Ergebnisse abrufen (bei
SELECT-Abfragen). - Das Statement schließen.
Prepared INSERT mit MySQLi
MySQLi verwendet positionelle ?-Platzhalter. Sie werden mit mysqli_stmt_bind_param() gebunden, wobei das erste Argument ein Typstring ist: s für string, i für integer, d für double/float, b für blob.
<?php
mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT); // throw on errors
$conn = mysqli_connect("localhost", "username", "password", "database");
$stmt = mysqli_prepare($conn, "INSERT INTO users (name, email) VALUES (?, ?)");
// "ss" => both placeholders are strings, in order
mysqli_stmt_bind_param($stmt, "ss", $name, $email);
$name = "John";
$email = "[email protected]";
mysqli_stmt_execute($stmt); // inserts John
$name = "Jane";
$email = "[email protected]";
mysqli_stmt_execute($stmt); // reuses the same compiled statement, inserts Jane
mysqli_stmt_close($stmt);
mysqli_close($conn);bind_param bindet per Referenz, sodass Sie $name/$email ändern und execute() erneut aufrufen können, ohne neu zu binden – die neuen Werte werden automatisch übernommen. Das ist der Vorteil von „einmal kompilieren, viele Male ausführen" in der Praxis.
Prepared SELECT mit MySQLi
Bei einem SELECT führen Sie das Statement aus und lesen anschließend Zeilen aus. Die sauberste Methode ist mysqli_stmt_get_result(), die ein normales Ergebnisset zurückgibt, über das Sie iterieren können:
<?php
mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT);
$conn = mysqli_connect("localhost", "username", "password", "database");
$stmt = mysqli_prepare($conn, "SELECT id, name FROM users WHERE email = ?");
mysqli_stmt_bind_param($stmt, "s", $email);
$email = "[email protected]";
mysqli_stmt_execute($stmt);
$result = mysqli_stmt_get_result($stmt);
while ($row = mysqli_fetch_assoc($result)) {
echo $row["id"] . ": " . $row["name"] . "\n";
}
mysqli_stmt_close($stmt);
mysqli_close($conn);Prepared Statements mit PDO
PDO ist die andere verbreitete Datenbankerweiterung. Viele Entwickler bevorzugen sie, weil sie über verschiedene Datenbanksysteme hinweg funktioniert und benannte Platzhalter (:email) unterstützt, die leichter zu lesen sind.
<?php
$pdo = new PDO(
"mysql:host=localhost;dbname=database;charset=utf8mb4",
"username",
"password",
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]
);
// INSERT with named placeholders
$stmt = $pdo->prepare("INSERT INTO users (name, email) VALUES (:name, :email)");
$stmt->execute([":name" => "John", ":email" => "[email protected]"]);
// SELECT and fetch
$stmt = $pdo->prepare("SELECT id, name FROM users WHERE email = :email");
$stmt->execute([":email" => "[email protected]"]);
foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $row) {
echo $row["id"] . ": " . $row["name"] . "\n";
}Beachten Sie, dass bei PDO keine Typen deklariert werden – Sie übergeben ein assoziatives Array mit Werten direkt an execute(). Das Setzen von PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION bewirkt, dass PDO bei einem Fehler eine Ausnahme wirft, sodass Probleme niemals stillschweigend ignoriert werden.
Häufige Fehler
- Fehlerreporting vergessen. Standardmäßig kann MySQLi still fehlschlagen. Rufen Sie
mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT)auf (oder verwenden Sie PDO-Exceptions), damit eine fehlerhafte Abfrage eine Ausnahme wirft, anstattfalsezurückzugeben. - Falschen Typanzahl binden. Der Typstring in
bind_parammuss genau ein Zeichen pro?enthalten."ss"für zwei Platzhalter,"si"für einen string und danach einen integer. - Platzhalter dort einsetzen, wo SQL keinen erlaubt. Sie können Werte binden, keine Bezeichner.
WHERE id = ?funktioniert;ORDER BY ?oderSELECT * FROM ?nicht – Tabellen- und Spaltennamen müssen fest kodiert oder per Whitelist geprüft werden. - „Nur diesen einen" Wert verketten. Es gibt keine sichere Ausnahme. Wenn er vom Benutzer stammt, binden Sie ihn.
Fazit
Prepared Statements teilen eine Abfrage in eine kompilierte SQL-Vorlage und die Werte, die sie ausfüllen, auf. Diese Trennung macht sie sowohl sicher (injection-geschützt) als auch schnell (einmal geparst, viele Male ausgeführt). Verwenden Sie MySQLis ?-Platzhalter oder PDOs benannte :value-Platzhalter, aber binden Sie Benutzereingaben immer, anstatt SQL von Hand zu bauen.
Lesen Sie weiter in den verwandten Kapiteln: Mit MySQL verbinden, Daten einfügen, Daten auswählen und die mysqli_prepare()-Referenz.