In PHP wandelst du Umlaut-Domains mit idn_to_ascii() in Punycode um, zurück geht es mit idn_to_utf8(). Beide stammen aus der intl-Erweiterung. Übergib immer IDNA_NONTRANSITIONAL_TO_ASCII und INTL_IDNA_VARIANT_UTS46, sonst wird aus straße.de strasse.de statt xn--strae-oqa.de. Fehler liest du aus dem vierten Parameter $info['errors'] aus.
Umlaut-Domains sind seit 2004 bei .de registrierbar, und trotzdem scheitern erstaunlich viele PHP-Anwendungen an ihnen. Meist nicht mit einer Fehlermeldung, sondern leise: Der DNS-Lookup für müller.de findet nichts, der Whois-Server antwortet mit „nicht gefunden“, und die Domain gilt als frei, obwohl sie längst vergeben ist.
Was Punycode ist und warum PHP es braucht
DNS kennt nur ASCII. Damit müller.de trotzdem funktioniert, definiert IDNA (Internationalizing Domain Names in Applications) eine Übersetzung: Jedes Label mit Nicht-ASCII-Zeichen wird mit dem Punycode-Verfahren aus RFC 3492 kodiert und bekommt das Präfix xn--. Aus müller wird xn--mller-kva.
Die Umwandlung passiert in Browsern und Mailclients automatisch. In deinem PHP-Code nicht. Jede Funktion, die mit dem DNS oder einer Registry spricht, also dns_get_record(), checkdnsrr(), fsockopen() für Whois oder ein RDAP-Request, braucht die ASCII-Form.
idn_to_ascii und idn_to_utf8 richtig aufrufen
Die beiden Funktionen kommen aus der intl-Erweiterung. Die Signatur hat vier Parameter, und alle sind wichtig:
<?php
declare(strict_types=1);
function toAscii(string $domain): string
{
$ascii = idn_to_ascii($domain, IDNA_NONTRANSITIONAL_TO_ASCII | IDNA_USE_STD3_RULES | IDNA_CHECK_BIDI | IDNA_CHECK_CONTEXTJ, INTL_IDNA_VARIANT_UTS46, $info);
if ($ascii === false || $info['errors'] !== 0) {
throw new InvalidArgumentException(sprintf('"%s" ist kein gültiger IDN (Fehlercode %d)', $domain, $info['errors'] ?? -1));
}
return $ascii;
}
function toUnicode(string $domain): string
{
$utf8 = idn_to_utf8($domain, IDNA_NONTRANSITIONAL_TO_UNICODE, INTL_IDNA_VARIANT_UTS46, $info);
return ($utf8 === false || $info['errors'] !== 0) ? $domain : $utf8;
}
foreach (['müller.de', 'straße.de', 'Bücher.DE', 'xn--strae-oqa.de', 'пример.рф', '日本.jp', 'exa mple.de', '-ü.de'] as $d) {
try {
$a = toAscii($d);
printf("%-18s -> %-24s -> %s\n", $d, $a, toUnicode($a));
} catch (InvalidArgumentException $e) {
echo $e->getMessage(), "\n";
}
}
Die Ausgabe unter PHP 8.5 mit ICU 78.3 (Spalten von Hand ausgerichtet):
müller.de -> xn--mller-kva.de -> müller.de
straße.de -> xn--strae-oqa.de -> straße.de
Bücher.DE -> xn--bcher-kva.de -> bücher.de
xn--strae-oqa.de -> xn--strae-oqa.de -> straße.de
пример.рф -> xn--e1afmkfd.xn--p1ai -> пример.рф
日本.jp -> xn--wgv71a.jp -> 日本.jp
"exa mple.de" ist kein gültiger IDN (Fehlercode 128)
"-ü.de" ist kein gültiger IDN (Fehlercode 8)
Bereits kodierte Namen bleiben unverändert, Großbuchstaben werden klein. Die Funktion ist also idempotent, du kannst sie gefahrlos auf jede Eingabe anwenden.
Die Flags: transitional, STD3, BIDI
Das wichtigste Flag ist IDNA_NONTRANSITIONAL_TO_ASCII. Ohne es arbeitet die Umwandlung im Übergangsmodus von IDNA2003 und bildet ß auf ss ab:
<?php
declare(strict_types=1);
var_dump(idn_to_ascii('straße.de', 0, INTL_IDNA_VARIANT_UTS46));
// string(10) "strasse.de"
var_dump(idn_to_ascii('straße.de', IDNA_NONTRANSITIONAL_TO_ASCII, INTL_IDNA_VARIANT_UTS46));
// string(16) "xn--strae-oqa.de"
Das ist kein Schönheitsfehler. strasse.de und straße.de sind zwei verschiedene Domains, die verschiedenen Leuten gehören können. Wer im Übergangsmodus prüft, fragt die falsche Domain ab. Unter PHP 8.5 hat die Konstante IDNA_DEFAULT übrigens den Wert 48, also beide Nontransitional-Flags. Verlass dich trotzdem nicht darauf und setz die Flags ausdrücklich, dann verhält sich dein Code auch auf älteren Servern gleich.
IDNA_USE_STD3_RULES lehnt Zeichen ab, die in Hostnamen nichts verloren haben, etwa Leerzeichen und Unterstriche. Ohne das Flag liefert idn_to_ascii('exa mple.de', ...) fröhlich exa mple.de zurück. IDNA_CHECK_BIDI und IDNA_CHECK_CONTEXTJ prüfen Regeln für Rechts-nach-links-Schriften und unsichtbare Verbindungszeichen. Für Eingaben aus Formularen schaden sie nie.
Fehlercodes auslesen
Der vierte Parameter $info enthält nach dem Aufruf result, isTransitionalDifferent und errors. errors ist eine Bitmaske. Die Konstanten, die du in der Praxis am häufigsten siehst:
| Konstante | Wert | Auslöser |
|---|---|---|
IDNA_ERROR_LABEL_TOO_LONG | 2 | Label über 63 Zeichen nach der Kodierung |
IDNA_ERROR_LEADING_HYPHEN | 8 | Label beginnt mit Bindestrich |
IDNA_ERROR_TRAILING_HYPHEN | 16 | Label endet mit Bindestrich |
IDNA_ERROR_HYPHEN_3_4 | 32 | -- an Position 3 und 4, z. B. ab--cd.de |
IDNA_ERROR_DISALLOWED | 128 | unzulässiges Zeichen |
Der Fehler 2 ist tückisch: 60 Umlaute sehen aus wie 60 Zeichen, werden kodiert aber zu einem Label mit über 63 Zeichen. Die Längenprüfung muss deshalb auf der ASCII-Form passieren, nie auf der Eingabe. Genau das macht der Regex im Beitrag Domain validieren mit Regex.
isTransitionalDifferent ist true, wenn transitionale und nicht-transitionale Verarbeitung verschiedene Ergebnisse liefern würden. Für straße.de ist das der Fall. Du kannst das Flag nutzen, um Nutzer zu warnen, dass es eine ähnlich aussehende Domain gibt.
Homographen: Domains, die gleich aussehen
IDN haben eine Schattenseite. Das kyrillische „а“ sieht aus wie das lateinische „a“, und exаmple.com mit kyrillischem а ist eine völlig andere Domain als example.com. In Punycode wird der Unterschied sichtbar: xn--exmple-4nf.com. Im Browser oft nicht, je nach Darstellungsregeln.
Die intl-Erweiterung bringt dafür die Klasse Spoofchecker mit:
<?php
declare(strict_types=1);
$checker = new Spoofchecker();
var_dump($checker->isSuspicious('exаmple.com')); // bool(true), kyrillisches а
var_dump($checker->areConfusable('exаmple.com', 'example.com')); // bool(true)
var_dump($checker->isSuspicious('müller.de')); // bool(false)
isSuspicious() schlägt bei gemischten Schriften an, müller.de bleibt unauffällig, weil ü zur lateinischen Schrift gehört. Sinnvoll ist das überall, wo Nutzer Links oder Absenderdomains sehen, die sie für vertrauenswürdig halten sollen: Admin-Oberflächen, Freigabelisten für Weiterleitungen, Support-Tools. Für ein normales Domain-Suchfeld ist es übertrieben, dort willst du ja gerade auch kyrillische Namen zulassen.
Wenn intl nicht installiert ist
Auf manchen Shared-Hostern fehlt die intl-Erweiterung. function_exists('idn_to_ascii') liefert dann false. Das Composer-Paket symfony/polyfill-intl-idn füllt die Lücke mit einer Implementierung in reinem PHP und denselben Konstanten. Es ist langsamer als die ICU-Variante, was bei ein paar Domains pro Request keine Rolle spielt. Einen Haken hat der Polyfill: Spoofchecker gehört nicht dazu. Prüf im Deployment deshalb mit php -m | grep intl, welche Variante auf dem Server tatsächlich läuft, statt dich auf die lokale Entwicklungsumgebung zu verlassen. Unterschiedliche ICU-Versionen können sich bei sehr neuen Unicode-Zeichen ebenfalls unterscheiden.
Wo Punycode im Code hingehört
Die Faustregel: Umwandeln an der Grenze nach außen, anzeigen an der Grenze zum Nutzer. Konkret:
- Vor jedem DNS-Lookup, Whois- oder RDAP-Request in ASCII umwandeln.
- In der Datenbank die ASCII-Form als eindeutigen Schlüssel speichern.
- In Oberflächen und Mails die Unicode-Form zeigen.
- Bei E-Mail-Adressen nur den Domainteil umwandeln, der lokale Teil vor dem @ folgt eigenen Regeln. Mehr dazu unter E-Mail-Validierung in PHP.
Ein Sonderfall ist die Zuordnung zu Endungen. xn--mller-kva.de endet auf .de, aber Endungen wie .рф sind selbst IDN. Für die Frage, welcher Teil eines Namens die registrierbare Domain ist, brauchst du die Public Suffix List in PHP, und die enthält beide Schreibweisen.
Ob eine Umlaut-Domain noch frei ist, prüfst du am einfachsten mit der Domainsuche von betadomain, die IDN direkt annimmt. Die Endung .de kostet dort 2,99 € im Jahr, die .at-Domain 13,99 €.
Domain gefunden? Prüfen und registrieren kannst du sie direkt bei betadomain – Jahrespreis ab 2,99 €.
Häufige Fragen
Was mache ich, wenn die intl-Erweiterung fehlt?
Installiere sie, auf Debian und Ubuntu etwa über das Paket php-intl. Geht das nicht, liefert symfony/polyfill-intl-idn die Funktionen idn_to_ascii() und idn_to_utf8() in reinem PHP nach. Es ist langsamer, aber für Formulareingaben völlig ausreichend.
Warum ist straße.de nicht dasselbe wie strasse.de?
Nach IDNA2008 ist ß ein eigenes Zeichen und wird zu xn--strae-oqa.de kodiert. Das ältere IDNA2003 bildete ß auf ss ab. Beide Domains können verschiedenen Inhabern gehören, deshalb ist die nicht-transitionale Verarbeitung die richtige.
Soll ich Domains in der Datenbank als Punycode oder Unicode speichern?
Speichere die ASCII-Form als Schlüssel für Vergleiche und Lookups und erzeuge die Unicode-Form für die Anzeige. So vermeidest du doppelte Einträge für denselben Namen in zwei Schreibweisen.
Welche Endungen erlauben Umlaute?
Viele, aber nicht alle. .de, .at, .ch und .eu erlauben Umlaute, .com ebenfalls. Welche Zeichen genau zulässig sind, legt jede Registry in ihrer IDN-Tabelle fest. Für .de veröffentlicht die DENIC eine feste Liste zusätzlicher Zeichen über a bis z hinaus.