Für ein kleineres PHP-Projekt meinerseits brauchte ich eine Anbindung an eine Datenbank. Da die Resourcen recht knapp auf dem System sind, ich aber eine Reihe von Dingen im Hintergrund erledigt brauche, habe ich mich ein wenig umgesehen, was PHP bietet, bzw. welche fertigen Libs es gibt. Anforderungen waren dabei bewusst einfach gewählt: Die Bibliothek der Wahl musste Prepared Statements unterstützen, sollte schlank sein und eine abgerissene Datenbank-Verbindung automatisch als solche erkennen und wiederherstellen können. Wie so oft bei PHP waren die meisten Implementierungen entweder zu aufgebläht, oder aber erfüllten die anderen Bedingungen nicht.
Am Ende blieb also lediglich, die Implementierung selber vorzunehmen. Da für mein Projekt die Anbindung an MySQL benötigt wurde, gab es nun allein bei PHP >5 Varianten, wie man hätte zu MySQL verbinden können:
- MySQL Extension –> Kann keine Prepared Statements
- MySQLi Extension –> Kann Prepared Statements, aber …
- MySQL Native Driver –> Kann Prepared Statements, aber …
- PDO (PHP Data Objects) –> Kann Prepared Statements, aber …
- ODBC oder andere Wrapper –> Können oft keine Prepared Statements oder sind zu langsam
Nimmt man bereits diese Vorauswahl, bleiben von den 5 Möglichkeiten noch 3 mit einem dicken ABER übrig. Schauen wir also einmal genauer und stelen fest:
- MySQLi Extension –> Bescheidenes Interface (gelinde gesagt)
- MySQL Native Driver –> Overkill für meine Anwendung
- PDO (PHP Data Objects) –> Wir wrappen einen Wrapper mit einem mWrapper, damit das Interface trotzdem Scheiße aussieht
Nehmen wir also den Native Driver einmal raus, verbleiben noch genau zwei Möglichkeiten, einen Datenbank-Layer zu implementieren, die sicherlich auch von den meisten verwendet werden: MySQLi oder PDO.
Wenn man sich nun anschaut, ob man mit einem bescheidenen Interface arbeiten muss oder gegen ein anderes, was genauso bescheiden ist, ist es einem im Endeffekt eigentlich egal, welches man davon nimmt. Vorteilhaft wäre für meinen Zweck zwar theoretisch PDO gewesen, da ich es gerne Datenbank-unabhängig gehabt hätte, aber das habe ich recht schnell sein gelassen. Der Grund dafür liegt in der Syntax der eingangs erwähnten Prepared Statements, die ich als Voraussetzung hatte.
Womit wir wieder bei dem Punkt bescheidene Interfaces wären: Ein Wunschkriterium war u.a. auch, dass der Datenbank-Layer Support für Named Parameters in Prepared Statements implementiert. Dies ist für PDO nicht der Fall. Vielmehr muss man durch massenhafte Rudelvorkommen von Fragezeichen die entsprechenden Stellen markieren, an denen man seine Parameter eingefügt haben möchte. Aus Perspektive der Lesbarkeit also das, was man zu allerletzt haben möchte. Aber auch MySQLi bestach in seinem Interface: So wurden zwar benannte Parameter unterstützt, „aus Kompatibilität“ wurde aber bei Mehrfach-Vorkommen eines Parameters immer nur das erste Vorkommen initialisiert – auch bei Mehrfach-Aufrufen. Stattdessen läuft man in Fehlermeldungen, man hätte zu wenige Parameter übergeben. Eindeutig ein FAIL! WER bitteschön denkt sich, dass man beim Bau einer Schnittstelle kompatibel zu den Fehlern anderer sein MUSS?!?!?!
Also gut. In meiner Bibliothek hab ich das jetzt insofern behoben, dass meine Bibliothek einen Workaround um diese Sinnlosigkeit macht: Wenn ich mehrfach den gleichen Namen vergebe, möchte ich auch, dass dort mehrfach der gleiche Wert ankommt. Und wie macht man das, wenn der zugrundeliegende Layer das nicht kann? Richtig, man emuliert es, durch Ersetzen von Variablen innerhalb der Query:
/**
* DBStatement::_prepareHelper()
*
* @param mixed $m
* @return
*/
private function _prepareHelper($m)
{
$paramname = $m[1];
if (!isset($this->params[$paramname])) {
$this->params[$paramname] = array();
}
$dbmsparam = $this->paramCount++;
$this->params[$paramname][] = $dbmsparam;
return "?";
}
/**
* DBStatement::reprepare()
*
* @return
*/
public function reprepare()
{
$this->params = array();
$this->paramCount = 0;
$_query = preg_replace_callback("/:(\w+)\b/", array($this, '_prepareHelper'), $this->query);
if (!$this->connection->ensureConnected()) {
return false;
}
$this->query_res = $this->connection->getHandle()->prepare($_query);
// ...
}
Dieser Source ist im Grund nix weiter, als ein stark vereinfachter SQL-Parser, der lediglich Parameter in einer SQL-Query findet. Es sind noch eine Reihe von Vereinfachungen enthalten (? wird nicht als Parameter erkannt), aber das lässt sich recht einfach nachrüsten. Auch das Ignorieren von Parametern innerhalb von Strings kann mit etwas Arbeit noch ergänzt werden. Wenn man aber bedenkt, dass man schon zu solchen Würgarounds greifen muss, um eine Abfrage vernünftig an MySQLi abzusetzen, ist man über solche Dinge wie „Wir binden das Ergebnis NUR an Variablen und verzichten auf die Rückgabe als Array“ schon fast eine Formalität. Also: Here we go (THX an Neo für das Finden dieses Tipps in den Kommentaren zur Dokumentation):
/**
* Bind the query to a variable for reading the result data
*
* @param mysqli_stmt $stmt
* @param mixed $out
* @return bolean Result of call.
*/
function stmt_bind_assoc(mysqli_stmt &$stmt, &$out) {
$data = mysqli_stmt_result_metadata($stmt);
$fields = array();
$out = array();
$fields[0] = $stmt;
$count = 1;
while ($field = mysqli_fetch_field($data)) {
$fields[$count] =& $out[$field->name];
$count++;
}
return call_user_func_array('mysqli_stmt_bind_result', $fields);
}
Wobei das Lesen nun wie von MySQL gewohnt erfolgen kann:
// Init the result buffer
$Result = array();
// Try to bind the result buffer to the query
if (!$this->stmt_bind_assoc($this->Query, $Result)) {
throw new Exception("Cannot bind the result buffer to the query object.");
return false;
}
// Fetch the result line by line
while ($this->Query->fetch()) {
// Fetch and store the data into the object's result buffer
$this->ResultSet[] = $Result;
}
Baut man nun noch ein wenig Magik drumherum, kann man daraus ein wunderbares, logisch strukturiertes Interface machen, was sich wie die alte MySQL-Extension verhält, sowohl Zeilenweise als auch insgesamt abrufbar ist und mit gerade einmal 3 Klassen eigentlich alles abdeckt, was man braucht:
- DBConnection: Das Herzstück jeder Verbindung zur Datenbank.
- DBStatement: Ein Prepared-Statement und Funktionen zu dessen Nutzung
- DBResultset: Ein Objekt, was jegliche Informationen über das Ergebnis einer Abfrage kapselt
Zusätzlich benötigt man zwar u.U. eine weitere Klasse, die die Login-Informationen beinhaltet, aber das lässt sich recht einfach realisieren. Verlgeicht man nun diese einfache Struktur mit dem Original-Interface von MySQLi, läuft es einem allein bei MySQLi kalt den Rücken runter: Nicht nur kann das Interface nicht sinnvoll dynamisch eingesetzt werden, erschwert wesentliche Aufgaben unnötig und liefert obendrein essentielle Informationen an völlig unlogischen Orten. Oder wer vermutet die letzte durch eine Abfrage generierte Datensatz-ID in der Verbindung, statt im Resultset-Objekt, was diese erzeugt hat? Um mal das Wort Konsistenz gar nicht weiter anzusprechen: Wenn man ein Prepared Statement ausführt, so muss man einige Ergebniswerte aus dem Prepared Statement lesen, obwohl diese zum Ergebnis gehören. Eben dieses bekommt man aber erst über zahlreiche Umwege und dann enthält es auch nur die Hälfte der Angaben. Also muss man in einem Wrapper sich Daten aus 4 Klassen (mysqli, mysqli_stmt, mysqli_result und stdclass-Objekten) zusammensuchen, statt als Ergebnis einer Abfrage ein Resultset zu bekommen, in dem alle nötigen Angaben zentral enthalten sind.
Sobald ich mit dem Aufräumen meines Datenbank-Layers und dessen Implementationn fertig bin, gibt es dazu auch noch ein Release.
Wer PHP als PHrickle-Programm bezeichnet, hat eindeutig Recht.