PHP und UTF-8 - eine Anleitung, Teil 3: PHP String-Funktionen

Es ist nun schon eine Weile her seit der letzten Folge, Zeit die unter anderem in den Gyro-PHP Application Framework geflossen ist. Dafür soll es in der Serie über PHP und UTF-8 nun endlich um PHP selber gehen. Dabei wird in einem ersten Schritt über die String-Funktionen von PHP zu reden sein, während die Frage der Regular Expressions auf eine weitere Folge vertagt ist.

Der grundlegende Unterschied zwischen Zeichensätzen wie Latin-1 aka ISO-8859-1 und UTF-8 ist, dass erstere pro Zeichen genau ein Byte brauchen, während ein Zeichen in UTF-8 beliebig viele Byte lang sein kann. Generell gilt die Regel, dass in UTF-8 die ASCII-Zeichen (das sind die Zeichen 1-128) ein Byte lang sind, die nächsten 1664 Unicode Zeichen (wozu auch das komplette Latin1 gehört) zwei Byte, und alle weiteren Zeichen (Kyrillisch, Indisch, Arabisch, etc.) drei Bytes.

Folgende Tabelle soll das illustrieren. Dargestellt werden die hexadezimalen Bytecodes eines Zeichen in Latin1 und UTF-8:

Zeichen Code Latin1 Code UTF-8 Länge UTF-8
A 0x41 0x41 1
Ä 0xC4 0xC384 2
± 0xB1 0xC2B1 2

Zu den drei Byte langen Zeichen gibt es naturgemäß in Latin-1 keine Entsprechungen.

Dass ein Zeichen in UTF-8 keine feste Länge mehr hat, stellt nun viele String-Funktionen in PHP vor ein großes Problem: Denn diese setzen vielfach voraus, dass ein Byte immer ein Zeichen ist. Augenfällig ist dies bei der strlen() Funktion.

<?php
print strlen('Ä'); // ergibt 2, wenn die Datei in UTF-8 ist
?>

Weitere Funktionen, die mit UTF-8 Probleme bekommen sind unter anderem strtoupper() und strtolower() sowie strpos(). Erstere würden beispielsweise das große Ä nicht erkennen, da es aus zwei Bytes besteht. strpos() leidet unter den gleichen Problemen wie strlen(), denn es zählt zur Positionsermittlung einfach Bytes.

Zum Glück ist die Abhilfe hier ziemlich einfach. Denn für jede Funktion, die mit UTF-8 nicht funktioniert, existiert ein Pendant in der PHP-Erweiterung Multibyte String. Die jeweilige Ersatz-Funktion ist namensgleich mit der originalen PHP-Funktion, hat aber ein “mb_” vorangestellt. So ist etwa mb_strtolower() die Mutibyte-Variante zu strtolower().

Die einzige Ausnahme ist die Funktion lcfirst(), die erst mit PHP 5.3 eingeführt wurde. Diese ist nicht UTF-8-fähig, und es gibt keine Mutibyte-Ersetzung.

Alle Multibyte-Funktionen, die PHP-Funktionen ersetzen, sind um einen Parameter erweitert, der die Zeichenkodierung aufnimmt.

<?php
$l = mb_strtolower('Äh...', 'UTF-8');
?>

Dieser Parameter ist optional, der Default-Wert kann über den Parameter mbstring.internal_encoding in der php.ini oder über die Funktion mb_internal_encoding() gesetzt werden.

<?php
mb_internal_encoding('UTF-8');
$l = mb_strtolower('Äh...');
?>

Es ist möglich, durch eine Einstellung in der php.ini alle relevanten PHP-String-Funktionen durch ihre Multibyte-Äquivalente zu ersetzten. Hier die Dokumentation dazu. In diesem Fall ist mb_internal_encoding() unbedingt aufzurufen, es sei denn, die Eigenschaft mbstring.internal_encoding ist in der php.ini bereits entsprechend gesetzt.

Das Überladen der originalen PHP-Funktionen sollte aber nur der letzte Ausweg sein, um bestehenden Code UTF-8 fähig zu machen, dessen Portierung zu umfangreich ist. Ansonsten sollten die mb-Funktionen immer vorgezogen werden, da damit auch für Dritte bereits im Code zu sehen ist, dass man sich Gedanken über UTF-8 gemacht hat.

Die Multibyte-Erweiterung bringt auch Funktionen für das Arbeiten mit regulären Ausdrücken mit. Allerdings nur für die Gruppe der ereg-Funktionen, die ab PHP 5.3 als “deprecated”, also veraltet, markiert sind und nicht mehr verwendet werden sollten. Die preg-Varianten der Regular Expressions sind ebenfalls nicht ohne weiteres UTF-8-fähig, dafür existieren aber keine Überladungen. Dies wird Thema der nächsten Folge sein.

Neben den klassischen String-Funktionen ist eine zweite Gruppe von Funktionen zu diskutieren, die für den Gebrauch mit UTF-8 angepasst werden müssen. Dies sind die Konvertierungsfunktionen, die Zeichen in ihre entsprechenden HTML-Entities umwandeln. Folgende Funktionen sind betroffen:

  • htmlentities()
  • html_entity_decode()
  • htmlspecialchars()

Jede dieser Funktionen aktzeptiert, das wird gerne vergessen, einen dritten Parameter, der den Zeichensatz bestimmt. Mit UTF-8 müssen diese also so aufgerufen werden:

<?php
$safe = htmlentities($unsafe, ENT_QUOTES, 'UTF-8');
$unsafe = html_entity_decode($safe, ENT_QUOTES, 'UTF-8');

$safe = htmlspecialchars($unsafe, ENT_QUOTES, 'UTF-8');
// htmlspecialchars_decode() funktioniert immer, denn <, >, & sind immer ASCII
$unsafe = htmlspecialchars_decode($unsafe, ENT_QUOTES);
?>

Leider führt das unter PHP 4.3 zu einem Fehler, auch wenn UTF-8 als Parameter laut Dokumentation unterstützt werden sollte.

Bei der Portierung existierenden Codes von Latin-1 zu UTF-8 ist es am einfachsten, für jede der drei Funktionen eine eigene Funktion zu schreiben, etwa indem man ein ‘utf8_’ davor schreibt:

<?php
/**
 * UTF-8 aware version of htmlentities()
 */
function utf8_htmlentities($text, $quote = ENT_COMPAT, $charset = 'UTF-8', $double_encode = TRUE) {
  return htmlentities($text, $quote, $charset, $double_encode);
}

/**
 * UTF-8 aware version of html_entity_decode()
 */
function utf8_html_entity_decode($text, $quote = ENT_COMPAT, $charset = 'UTF-8') {
  return html_entity_decode($text, $quote, $charset);
}

/**
 * UTF-8 aware version of htmlspecialchars()
 */
function utf8_htmlspecialchars($text, $quote = ENT_COMPAT, $charset = 'UTF-8', $double_encode = TRUE) {
  return htmlspecialchars($text, $quote, $charset, $double_encode);
}
?>

Jetzt können die alten Aufrufe einfach per Suchen und Ersetzen durch ihre UTF-8-Varianten ersetzt werden.

An dieser Stelle sei gesagt, dass es generell eine gute Idee ist, Funktionen mit vielen konstanten Parametern, wie etwa hier der Quote-Style und der Charset, von vornherein in eine eigene Funktion zu wickeln. Dies macht es einfach, das Standardverhalten später zu ändern.

Published: February 18 2010