Verwerking in PHP

Welke encoding?
Met welke encoding worden gegevens in PHP verwerkt? Helaas is daarover veel te zeggen, het verschilt per situatie:
* SBCS-functies roepen onderhuids C-functies aan, hoe het werkt hangt af van de locale, de build, en het besturingssysteem.
* Verschillende extenties kennen hun eigen instellingen, zoals iconv, mb_*()-functies en anderen.
* Sommige functies zijn compleet apatisch, zoals utf8_encode() en utf8_decode.
* Sommige extenties werken uitsluitend met UTF-8, zoals XMLWriter en DOMDocument
* Sommige functies als utf8_encode() werken alleen met de Basic Multilingual Plane, vergelijkbaar met de "utf8" encoding van MySQL.

Eh, locale?
Een locale is een combinatie van instellingen voor een bepaalde cultuur, onder meer getalnotatie, leesrichting, karakterset, en collatie.
Ter informatie: een collatie kun je zien als een sorteervolgorde van een bepaalde karakterset, je kunt per karakterset soms uit meerdere collaties kiezen.

Om te achterhalen welke karakterset PHP gebruikt voor de meest normale SBCS-functies als strtoupper() moeten we zijn bij setlocale(). Deze functie komt overeen met de setlocale() van de programmeertaal C. Ofwel ASCII-karakters gaan wel goed, maar voor alle overige codepages moeten we bij het besturingssysteem zijn.
Als we kijken op een Linux-testsysteem dan blijkt een standaardinstelling van PHP 5.5 "en_US.UTF-8" te zijn:

Code (php)
PHP script in nieuw venster Selecteer het PHP script
1
2
3
<?php
print setlocale(LC_CTYPE, 0);
?>

Het deel voor de punt is de land/regio-code, en na de punt komt de encoding. Heel bemoedigend lijkt het dat de encoding staat ingesteld op 'UTF-8', maar betekent dat dan ook dat strtoupper() werkt met UTF-8?
Helaas niet! De encoding is die van het besturingssysteem, en de SBCS-functies van PHP zijn slechts wrappers van hun C-equivalent. Dus hangt de werking af van de implementatie van de compiler waarmee PHP is gebouwd, èn van het besturingssysteem. De C-functies werken alleen met SBCS-encodings, niet met een VMBCS-encoding als UTF-8. Zie ook: http://www.cplusplus.com/reference/cctype .

Thread safety
Een bijkomend issue is dat de functie setlocale() niet altijd thread safe is. Afhankelijk van hoe PHP is gecompileerd en met welke webserver het wordt gebruikt is PHP wel of niet thread safe met andere concurrent PHP threads. Op Windows bijvoorbeeld wordt voor Apache een thread safe (TS) build gebruikt, en voor Microsoft IIS een non-thread safe (NTS) build. Zie ook: http://windows.php.net/download. Op de NTS build verandert de setlocale() functie de locale voor andere PHP threads. Dit aspect van setlocale() komt minder vaak voor, PHP wordt doorgaans gebruikt in een Linux-Apache-MySQL-PHP stack, waar op Windows meestal een combinatie van Microsoft oplossingen wordt gebruikt: IIS, ASP en .NET.

Oude en nieuwe functies
Kort en goed kunnen we verwerking van UTF-8 binnen PHP dus niet aan de oude C-functies overlaten, waarmee zo'n beetje alle PHP-functies voor string-manipulatie werken. Die zijn niet bruikbaar met UTF-8. UTF-8 kan per codepoint meerdere bytes gebruiken waar de SBCS-functies daar totaal geen rekening mee houden. De oude functies maken UTF-8 encoding per definitie corrupt. Een bijkomend voordeel is dat we niet hoeven te letten op verschillen tussen TS en NTS builds.

PHP heeft twee onmisbare extenties die het mogelijk maken om met MBCS te werken: iconv en Multibyte Strings. De iconv-extentie is uitermate geschikt voor transcoding van en naar Unicode. Multibyte String voorziet in basisbewerkingen van UTF-8. Beide extenties zijn echter optioneel!
De iconv-extentie wordt standaard meegecompileerd met PHP, maar Multibyte String niet. Je kunt natuurlijk met phpinfo() kijken of de extenties zijn ingeschakeld. Programmatisch controleren is nog mooier, met bijvoorbeeld function_exists().

Instellingen
We kunnen ons best doen om aan het begin van de PHP-applicatie alles zo goed mogelijk in te stellen:
Code (php)
PHP script in nieuw venster Selecteer het PHP script
1
2
3
4
5
6
<?php
setlocale(LC_CTYPE, '.UTF-8');                      // SBCS-functies voor wat het waard is
@iconv_set_encoding('internal_encoding', 'UTF-8');  // iconv -extentie
@mb_internal_encoding('UTF-8');                     // mb_*()-functies
ini_set('default_charset', 'UTF-8');                // html*(). En iconv en mb_*()-functies sinds PHP 5.6
?>


iconv
Een praktische toepassing is om Unicode te transcoderen naar ASCII, bijvoorbeeld bij het automatisch genereren van SEO-friendly URL's, XML-nodes, en dergelijke. Er is een mapping tussen diakritische karakters in Unicode en gewone letters in ASCII.
Code (php)
PHP script in nieuw venster Selecteer het PHP script
1
2
3
<?php
echo iconv('UTF-8', 'ASCII//TRANSLIT', "Cliënt"); // PHP code is UTF-8, uitvoer: "Client" in ASCII
?>


Multibyte String
De Multibyte String extentie biedt basale ondersteuning voor MBCS-functies. In nieuwe en bestaande PHP-code zijn de SBCS-functies aan te passen door de mb_*-variant. Helaas bestaat er voor lang niet elke SBCS-functie een MBCS-variant: http://php.net/manual-lookup.php?pattern=mb_
In de PHP-code kunnen slechts een aantal functies direct worden omgezet:
* substr() -> mb_substr()
* strrpos() -> mb_strrpos()
* strtoupper() -> mb_strtoupper()
* strripos() -> mb_strripos()
* stripos() -> mb_stripos()
* strpos() -> mb_strpos()
* strstr() -> mb_strstr()
Hier houdt de vergelijking wel zo'n beetje op. Multibyte String heeft naast deze functies ook nog andere functies waarvoor geen SBCS-variant bestaat, en ook nog een aantal regex-, e-mail-, transcoding-, detectie- en HTTP-functies. Al met al voldoende om alles te kunnen. Maar overzetten van bestaande PHP-code kan een hele klus zijn, afhankelijk van de omvang van de applicatie. Overal waar strings bewerkt worden moet de PHP-code mogelijk herzien worden.

Strings als arrays
Je kunt in PHP bij de verschillende bytes van een string komen:
Code (php)
PHP script in nieuw venster Selecteer het PHP script
1
2
3
4
5
6
7
8
9
<?php
$tekst
= 'Hallo cliënt'; // opgeslagen als Latin1
$lengte = strlen($tekst); // lengte in bytes
for($i = 0; $i < $lengte; $i++) {
  $karakter = $tekst[$i];
  if ($karakter == 'ë') {print 'e';}
  else {print $karakter;}
}

?>

Deze code levert problemen op wanneer het wordt opgeslagen als UTF-8, en moet herschreven naar bijvoorbeeld:
Code (php)
PHP script in nieuw venster Selecteer het PHP script
1
2
3
4
5
6
7
8
9
<?php
$tekst
= 'Hallo cliënt'; // opgeslagen als UTF-8
$lengte = mb_strlen($tekst);
for($i = 0; $i < $lengte; $i++) {
  $karakter = mb_substr($tekst, $i, 1);
  if ($karakter == 'ë') {print 'e';}
  else {print $karakter;}
}

?>

De array access methode in PHP werkt niet op basis van karakters, maar op basis van bytes, voor karakters van MBCS-strings gaat werkt die manier dan ook niet, gebruik van mb_substr() kan een alternatief zijn.

HTML-functies
PHP biedt functies voor de ondersteuning van HTML-entiteiten: html_entity_decode(), htmlentities(), htmlspecialchars(), htmlspecialchars_decode(). Deze functies worden nog wel eens gebruikt om incidentele encoding-problemen op te lossen door ze te vermijden met ASCII, bijvoorbeeld zo:
Code (php)
PHP script in nieuw venster Selecteer het PHP script
1
2
3
4
5
<?php
header('Content-Type:text/plain; charset=ISO-8859-1'); // Latin1
print "Cliënt\n"; // Code opgeslagen als UTF-8, gaat fout
print htmlentities("Cliënt\n"); // wordt "Cli&euml;nt" (ASCII), gaat goed
?>

Echter, als alles Unicode is, zijn de htmlentities niet nodig voor het omzeilen van transcoding.

Transcoding in PHP
Een vaak voorkomende manier om te schipperen tussen verschillende SBCS- en MBCS-encodings in PHP is met de functies utf8_encode() en utf8_decode(). Het onthoudt wel makkelijk, maar het transcodeert uitsluitend tussen ISO-8859-1 en UTF-8, waardoor de meerwaarde van UTF-8 verloren gaat, karakters buiten ISO-8859-1 kunnen niet gebruikt worden. Daarbij ondersteunen de functies niet volledig UTF-8, alleen de eerste Basic Multilingual Plane plane. Een plane is een groep van 64k codepoints. Unicode kent 17 planes, maar voor conversie van en naar Latin1 is meer ook eigenlijk niet nodig. Het is vergelijkbaar met de "utf8" van MySQL.

JSON
Voor applicaties die gebruik maken van JSON is het handig om te weten dat json_encode() standaard codepoints noteert in de Unicode escape sequence, die begint met /u of /U met daarna in hexadecimale notatie de bytes van het UTF-8 codepoint. Dat kost extra data, en het kan vervelend lezen voor de meeste mensen. Gebruik van de JSON_UNESCAPED_UNICODE optie is dan aan te bevelen:
Code (php)
PHP script in nieuw venster Selecteer het PHP script
1
2
3
4
<?php
var_dump(json_encode("<SNOWMAN>")); // resultaat "\u2603", wat lang.
var_dump(json_encode("<SNOWMAN>", JSON_UNESCAPED_UNICODE)); // resultaat "<SNOWMAN>", beter.
?>

<SNOWMAN> staat hier voor het Unicode karakter 2603, dat momenteel niet weergegeven kan worden via PHPHulp.nl vanwege de Latin1-encoding. Zie ook:
http://www.fileformat.info/info/unicode/char/2603

Limitaties
Pas vanaf PHP 7 komt er een escape syntax speciaal voor Unicode codepoints in PHP-code. Zie ook: https://wiki.php.net/rfc/unicode_escape
Tot die tijd is een programmeur volledig aangewezen op Unicode support van de PHP-editor of IDE.

Liever niet in PHP
Gezien de wat karige ondersteuning vanuit PHP voor Unicode, is het bewerken van UTF-8 in PHP niet echt eenvoudig te noemen.
Er zijn op het internet third party code te vinden met een meer volledig aanbod aan MBCS-functies. Ook bekende frameworks bieden dit. Dat maakt het werken met Unicode minder omslachtig, maar ook afhankelijker van third party tooling.
Zie ook: http://www.sitepoint.com/bringing-unicode-to-php-with-portable-utf8

Gebruik databases
Een best practice zou kunnen zijn om zo min mogelijk MBCS-stringbewerkingen in PHP zelf te doen. De term in-database processing is van toepassing: bewerk de data in de database waar het al is. Maak dankbaar gebruik van VIEWs, CHECKs, REFERENCEs en CONSTRAINTs, en vooral STORED PROCEDURES. Databases zijn beter uitgerust voor dit type bewerking, en dan hoeft PHP zo min mogelijk te doen. Door de balans te verschuiven richting een database win je aan functionaliteit en performance. Een tradeoff is flexibiliteit; je zit meer vast aan database-specifieke code.

« Lees de omschrijving en reacties

 
 

Om de gebruiksvriendelijkheid van onze website en diensten te optimaliseren maken wij gebruik van cookies. Deze cookies gebruiken wij voor functionaliteiten, analytische gegevens en marketing doeleinden. U vindt meer informatie in onze privacy statement.