Ein internes Werkzeug soll das Ergebnis beliebiger SELECT-Abfragen anzeigen, auch solcher, die jemand gerade erst ins Eingabefeld tippt. Die Kopfzeile aus dem ersten Datensatz abzuleiten liegt nahe und geht genau dann schief, wenn die Abfrage keine einzige Zeile zurückgibt. Dann steht die Tabelle ohne Überschriften da und sieht nach einem Fehler aus, obwohl alles richtig lief. PHP mysqli_fetch_field löst das sauber, denn die Spalteninformation hängt am Ergebnis und nicht an den Daten darin. Der Weg zum Ergebnis, also Verbindung, Platzhalter und get_result(), steht im Tutorial zu Prepared Statements und wird hier vorausgesetzt.
An jeder Spalte einer Ergebnismenge hängt eine Karte mit Name, Typ, Länge und Flags, unabhängig davon, ob auch nur eine Zeile zurückkommt. Diese Karte ist das Feldobjekt.
Was PHP mysqli_fetch_field() liefert
PHP mysqli_fetch_field() gibt ein Objekt heraus, das eine einzelne Spalte der Ergebnismenge beschreibt. Ein Aufruf, ein Feldobjekt, und der interne Feldzeiger rückt dabei eine Position weiter. Am schnellsten begreift man den Umfang, indem man das erste Objekt einmal vollständig ausgibt.
<?php
$sql = 'SELECT id, name FROM kunden';
$res = $mysqli->query($sql);
$feld = $res->fetch_field();
print_r($feld);
/* stdClass Object (
[name] => id
[orgname] => id
[table] => kunden
[orgtable] => kunden
[def] =>
[db] => shop
[catalog] => def
[max_length] => 0
[length] => 11
[charsetnr] => 63
[flags] => 49667
[type] => 3
[decimals] => 0
) */
Dreizehn Eigenschaften, von denen im Alltag etwa fünf gebraucht werden. Die wichtigsten stehen in der Tabelle, zusammen mit dem, was bei einer berechneten Spalte wie SELECT COUNT(*) AS anzahl davon übrig bleibt. Diese dritte Spalte ist kein Detail: Wer blind auf orgtable zugreift, baut sich eine Stelle, die später still einen leeren String liefert. PHP mysqli_fetch_field() meldet dabei keinen Fehler, es steht schlicht nichts darin.
| Eigenschaft | Was darin steht | Bei COUNT(*) AS anzahl |
| name | Spaltenname, bei einem Alias der Alias | anzahl |
| orgname | Name der Spalte im Schema, ohne Alias | leer |
| table | Tabellenname oder der Alias aus dem JOIN | leer |
| orgtable | Tabelle im Schema, ohne Alias | leer |
| type | Typnummer des Datentyps, siehe unten | 8 |
| length | die im Schema festgelegte Größe | 21 |
| max_length | längster Wert, der im Ergebnis vorkam, nur bei gepuffertem Ergebnis gesetzt | je nach Datenlage |
| flags | Bitmaske mit den Eigenschaften der Spalte | Zahlenbits, kein Schlüsselbit |
| decimals | Nachkommastellen bei Dezimalzahlen | 0 |
| charsetnr | Nummer der Kollation, 63 steht für binary und kennzeichnet damit Zahl- und BLOB-Spalten | 63 |
| def | Standardwert der Spalte | bleibt leer, mysqli füllt dieses Feld nie |
Syntax: die Signatur im Original
PHP mysqli_fetch_field() nimmt einen einzigen Parameter und antwortet mit einem Objekt oder mit false:
<?php
mysqli_fetch_field(
mysqli_result $result
): object|false
/* result: a result set returned by
mysqli_query(), mysqli_store_result(),
mysqli_use_result() or get_result()
returns an object holding the definition
of the NEXT column and advances the
internal field pointer, or false when
no column is left
object style: $result->fetch_field() */
Das Wort next im Handbuch trägt mehr Gewicht, als es aussieht. PHP mysqli_fetch_field() ist keine Abfragefunktion, die immer dasselbe liefert, sondern eine Lesefunktion mit Zustand. Der Rückgabewert false bedeutet deshalb nicht, dass es keine Spalten gibt, sondern nur, dass keine mehr übrig ist.
Der Feldzeiger von PHP mysqli_fetch_field() und field_seek()
Diese Eigenheit kostet Zeit, wenn man sie nicht kennt, denn sie meldet sich nirgends. Jeder Aufruf rückt den Feldzeiger eine Position weiter, nach dem letzten Feld bleibt er am Ende stehen. Eine zweite Schleife über dieselbe Ergebnismenge läuft dann sofort ins Leere, ohne dass irgendetwas meldet, warum.
flowchart TD
A[Abfrage ausführen] --> B[Ergebnis]
B --> C[Zeiger auf Feld 0]
C --> D{Noch ein Feld?}
D -- ja --> E[Feldobjekt lesen]
E --> D
D -- nein --> F[false, Zeiger am Ende]
F --> G[field_seek 0]
G --> C
<?php
$res = $mysqli->query('SELECT id, name FROM kunden');
while ($f = $res->fetch_field()) {
echo $f->name, ' ';
}
/* id name */
while ($f = $res->fetch_field()) {
echo $f->name, ' ';
}
/* keine Ausgabe, der Zeiger steht am Ende */
$res->field_seek(0);
while ($f = $res->fetch_field()) {
echo $f->name, ' ';
}
/* id name */
Es gibt drei Wege durch die Felder, und sie schließen sich nicht aus. fetch_fields() holt alle Feldobjekte auf einmal in ein Array, das sich beliebig oft durchlaufen lässt. fetch_field_direct($index) greift ein einzelnes Feld über seine Position heraus und bewegt den Zeiger nicht. Und PHP mysqli_fetch_field() geht Feld für Feld vor, was sich lohnt, wenn die Schleife vorzeitig abbrechen soll.
<?php
$felder = $res->fetch_fields();
foreach ($felder as $f) {
echo $f->name, PHP_EOL;
}
foreach ($felder as $f) {
echo $f->type, PHP_EOL;
}
$zweites = $res->fetch_field_direct(1);
echo $zweites->name, PHP_EOL;
Im Normalfall ist fetch_fields() die bequemere Wahl, weil das Ergebnis mehrfach gelesen werden kann und kein Zeiger im Weg steht. Wer stattdessen PHP mysqli_fetch_field() nimmt und später noch einmal von vorn anfangen muss, setzt field_seek(0) davor. Das kostet nichts und erspart die Fehlersuche.
Praxis: ein generischer Tabellen-Renderer
Der erste Baustein nimmt ein beliebiges Ergebnis entgegen und gibt HTML zurück. Die Kopfzeile stammt aus den Feldnamen, die Datenzeilen aus fetch_row(). Entscheidend ist, was passiert, wenn kein Datensatz zurückkommt: Die Kopfzeile steht trotzdem, und die Tabelle bleibt leer statt kaputt.
Die Metadaten aus PHP mysqli_fetch_field() sind hier die einzige Quelle für die Überschriften, und das ist Absicht. Eine Kopfzeile aus array_keys() des ersten Datensatzes sieht kürzer aus und bricht bei jedem leeren Ergebnis. Der Aufruf von htmlspecialchars() auf dem Feldnamen wirkt an dieser Stelle übervorsichtig, weil Spaltennamen aus dem Schema stammen. Bei einem Alias jedoch steht dort, was der Benutzer in die Abfrage geschrieben hat.
<?php
function tabelle(mysqli_result $res): string
{
$felder = $res->fetch_fields();
$html = '<table><tr>';
foreach ($felder as $f) {
$html .= '<th>'
. htmlspecialchars($f->name)
. '</th>';
}
$html .= '</tr>';
while ($zeile = $res->fetch_row()) {
$html .= '<tr>';
foreach ($zeile as $wert) {
$html .= '<td>'
. htmlspecialchars((string) $wert)
. '</td>';
}
$html .= '</tr>';
}
return $html . '</table>';
}
CSV-Export ohne feste Spaltennamen
Derselbe Gedanke trägt den Export. Die Kopfzeile entsteht aus den Feldnamen, und weil in name der Alias steht, bestimmt die Abfrage die Beschriftung der Datei. Wer SELECT nachname AS Familienname schreibt, bekommt genau das in der ersten Zeile, ohne eine Zuordnungstabelle im Code zu pflegen. Hier schreibt fputcsv() Zeile für Zeile in einen offenen Datenstrom, was bei großen Ergebnissen der richtige Weg ist. Wer die Datei stattdessen am Stück ablegt, findet die Fallstricke im Tutorial zu file_put_contents(). Die erste Zeile stammt in beiden Fällen aus PHP mysqli_fetch_field().
<?php
$datei = fopen('export.csv', 'w');
$kopf = [];
foreach ($res->fetch_fields() as $f) {
$kopf[] = $f->name;
}
fputcsv($datei, $kopf, ';');
while ($zeile = $res->fetch_row()) {
fputcsv($datei, $zeile, ';');
}
fclose($datei);
Typnummern in lesbare Namen übersetzen
In der Eigenschaft type des Objekts, das PHP mysqli_fetch_field() liefert, steht eine Zahl. Die vollständige Liste der Konstanten im Handbuch nachzuschlagen lohnt sich selten, weil im Alltag eine Handvoll Nummern fast alles abdeckt. Nutzbringender als der Name ist ohnehin die Gruppe: Zahlen werden rechtsbündig ausgegeben, Datumswerte ins deutsche Format gebracht, alles andere bleibt linksbündig. Fertige Formatierungsmuster dafür liefert sprintf().
| type | Konstante | Spaltentyp in MySQL |
| 3 | MYSQLI_TYPE_LONG | INT |
| 8 | MYSQLI_TYPE_LONGLONG | BIGINT, auch COUNT(*) |
| 10 | MYSQLI_TYPE_DATE | DATE |
| 12 | MYSQLI_TYPE_DATETIME | DATETIME |
| 246 | MYSQLI_TYPE_NEWDECIMAL | DECIMAL, etwa Preise |
| 252 | MYSQLI_TYPE_BLOB | TEXT und BLOB |
| 253 | MYSQLI_TYPE_VAR_STRING | VARCHAR |
Zwei Spaltentypen verstecken sich hinter einer fremden Nummer. Eine ENUM-Spalte meldet den Typ 254, also MYSQLI_TYPE_STRING, und setzt zusätzlich MYSQLI_ENUM_FLAG. Eine SET-Spalte verhält sich genauso und setzt MYSQLI_SET_FLAG. Wer aus den Metadaten ein Formular baut, erkennt daran die Spalten, aus denen ein Auswahlfeld statt eines Textfelds werden soll. Die erlaubten Werte selbst stehen allerdings nicht im Feldobjekt, dafür führt kein Weg an SHOW COLUMNS vorbei.
<?php
const TYP_GRUPPEN = [
'zahl' => [1, 2, 3, 8, 9, 4, 5, 246],
'datum' => [7, 10, 11, 12, 13, 14],
];
function typgruppe(int $typ): string
{
foreach (TYP_GRUPPEN as $name => $liste) {
if (in_array($typ, $liste, true)) {
return $name;
}
}
return 'text';
}
foreach ($res->fetch_fields() as $f) {
$gruppe = typgruppe($f->type);
$klasse = $gruppe === 'zahl' ? 'rechts' : 'links';
echo $f->name, ': ', $gruppe, ' / ', $klasse;
echo PHP_EOL;
}
Flags auswerten: Primärschlüssel und Pflichtfeld erkennen
Die Eigenschaft flags ist eine Bitmaske, in der mehrere Aussagen zugleich stecken. Sie ist die aufschlussreichste Angabe, die PHP mysqli_fetch_field() mitbringt. Deshalb wird sie mit dem bitweisen Und abgefragt und niemals mit == verglichen. Ein Vergleich $f->flags == 2 schlägt fehl, sobald neben dem Schlüsselbit noch irgendein anderes gesetzt ist, und das ist bei einer Primärschlüsselspalte immer der Fall.
| Konstante | Wert | Bedeutung |
| MYSQLI_NOT_NULL_FLAG | 1 | Spalte erlaubt kein NULL, also Pflichtfeld |
| MYSQLI_PRI_KEY_FLAG | 2 | Teil des Primärschlüssels |
| MYSQLI_UNIQUE_KEY_FLAG | 4 | Teil eines eindeutigen Index |
| MYSQLI_BLOB_FLAG | 16 | TEXT oder BLOB, besser nicht in die Liste |
| MYSQLI_UNSIGNED_FLAG | 32 | Zahl ohne Vorzeichen |
| MYSQLI_AUTO_INCREMENT_FLAG | 512 | Wert vergibt die Datenbank selbst |
| MYSQLI_ENUM_FLAG | 256 | Spalte ist ein ENUM, gemeldet wird trotzdem Typ 254 |
| MYSQLI_SET_FLAG | 2048 | Spalte ist ein SET |
<?php
$bits = [
MYSQLI_NOT_NULL_FLAG => 'Pflichtfeld',
MYSQLI_PRI_KEY_FLAG => 'Primärschlüssel',
MYSQLI_AUTO_INCREMENT_FLAG => 'Autowert',
];
foreach ($res->fetch_fields() as $f) {
$treffer = [];
foreach ($bits as $bit => $text) {
if ($f->flags & $bit) {
$treffer[] = $text;
}
}
echo $f->name, ': ';
echo implode(', ', $treffer), PHP_EOL;
}
/* id: Pflichtfeld, Primärschlüssel, Autowert */
/* name: Pflichtfeld */
Aus diesen drei Zeilen lässt sich ein Formulargenerator bauen, der Pflichtfelder markiert und den Autowert gar nicht erst anbietet. Zusammen mit den Typnummern aus dem Abschnitt davor entsteht daraus ein Eingabefeld, das zur Spalte passt, und zwar ohne eine einzige fest verdrahtete Angabe im Code.
name gegen orgname, table gegen orgtable
Die beiden Paare unterscheiden zwischen dem, was in der Abfrage steht, und dem, was im Schema steht. Bei SELECT k.nachname AS Familienname liefert name den Alias und orgname den echten Spaltennamen. Genauso verhält es sich mit table und orgtable, sobald ein JOIN mit Tabellenaliassen arbeitet. Bei berechneten Spalten bleiben die Herkunftsangaben leer, weshalb der Zugriff einen Rückfall braucht.
<?php
$sql = 'SELECT k.nachname AS Familienname, '
. 'COUNT(b.id) AS anzahl '
. 'FROM kunden k '
. 'LEFT JOIN bestellungen b ON b.kunde = k.id '
. 'GROUP BY k.id';
$res = $mysqli->query($sql);
foreach ($res->fetch_fields() as $f) {
echo $f->name, ' | ';
echo $f->orgname ?: 'berechnet', ' | ';
echo $f->table ?: '-', ' | ';
echo $f->orgtable ?: '-', PHP_EOL;
}
/* Familienname | nachname | k | kunden */
/* anzahl | berechnet | - | - */
Wer eine Bearbeitungsmaske aus einem Ergebnis ableitet, braucht diese Unterscheidung zwingend: Zurückgeschrieben wird nach orgtable und orgname, angezeigt wird name. Und eine Spalte ohne orgtable ist berechnet und damit nicht schreibbar. Eine Zeile Prüfung auf orgtable davor, und ein UPDATE, das nie funktionieren kann, kommt gar nicht erst zustande.
Der Blick zu PDO: getColumnMeta()
In PDO beantwortet PDOStatement::getColumnMeta() dieselbe Frage. Der Unterschied liegt weniger in den Daten als in der Zusage: mysqli sichert die Metadaten fest zu, PDO überlässt sie dem Treiber und bezeichnet die Methode im Handbuch ausdrücklich als nicht überall vorhanden. Dazu kommt, dass ein Array mit anderen Schlüsselnamen zurückkommt statt eines Objekts. Wer auf mysqli setzt, hat mit PHP mysqli_fetch_field() die verlässlichere Auskunft.
Fazit
Das Feldobjekt beantwortet drei Fragen auf einmal: wie die Spalte heißt, welchen Typ sie hat und welche Regeln für sie gelten. Damit lässt sich eine beliebige Abfrage ausgeben, ohne ihre Struktur zu kennen, und die Kopfzeile steht auch bei null Treffern. Der Feldzeiger bewegt sich bei jedem Aufruf, weshalb fetch_fields() im Zweifel der ruhigere Weg ist, und flags ist eine Bitmaske, die man mit dem bitweisen Und liest. Wer das beherzigt, hat mit PHP mysqli_fetch_field() zwei Bausteine, die in jedes eigene Adminwerkzeug passen.