Tutorials

Alles over Unicode

Voor iedere PHP-er, wat nou precies de problemen zijn met vreemde tekens en hoe die definitief op te lossen.

Pagina 1

Inleiding

Doelgroep
Voor iedere PHP-er die wel eens problemen heeft met 'vreemde tekens' of diakritische tekens in de data, na het lezen van deze tutorial begrijp je waar en waarom het misgaat, en wat je er aan kunt doen. Problemen verdwijnen als sneeuw voor de zon.
Ook biedt het inzicht in de materie voor ervarener PHP-ers.

Onderwerp
Soms werken pluspunten van PHP tegen je. PHP is laagdrempelig en een programmeur hoeft niet te veel na te denken over de verschillende typen data in PHP, omdat er automatische conversie plaatsvindt (typecasting). Een variabele is niet gebonden aan een vast datatype, maar kan van datatype veranderen tijdens het uitvoeren van het PHP-script. PHP is een zogeheten weakly typed-programmeertaal.
Waar programmeurs zich niet altijd van bewust zijn, is dat ook alle tekst (strings) een eigen data-type (encoding) heeft. En PHP houdt daar geen rekening mee. Er vindt geen automatische conversie (transcoding) plaats tussen de encodings. De programmeur moet dit zelf doen.

Noodzaak
Het niet of verkeerd transcoderen van encodings leidt tot het corrupt raken van data en soms ook van programmatuur. Verschilende functies en exenties van PHP werken anders bij verschillende encodings. Hoewel de meestgebruikte functies binnen PHP niet geschikt zijn voor Unicode, is Unicode vanaf PHP 5.6 de standaard-encoding. Deze plotselinge overgang kan voor verwarring zorgen.
De standaard-encoding van het web (HTML5 en JavaScript) is tevens Unicode. Om goede programma's te kunnen schrijven is kennis van transcoding tussen encodings nodig.

Checklist
Voor nieuwe en bestaande applicaties is er een uitgewerkte checklist die helpt om vreemde tekens te voorkomen. De lijst is door de auteur door de tijd heen verzameld van verschillende sites en blogs, en toegepast in verschillende praktijksituaties.

Jargon
In de tutorial wordt het nodige aan jargon gebruikt, dat schuingedrukt wordt weergegeven. Kennis daarover wordt veronderstelt. Alle termen zijn na te zoeken op het web.
Pagina 2

Over Unicode

Wat is Unicode nou eigenlijk?
Unicode is een wereldwijd consortium, dat de gelijknamige standaard heeft bedacht om alles wat mensen schrijven in een enkele set vast te leggen, met een uniek nummer voor alle denkbare karakters: letters, leestekens, besturingstekens, symbolen, smileys en zelfs zelfverzonnen symbolen.
Er zijn ruwweg 1.114.112 karakters gedefineerd, en Unicode is tot op de dag van vandaag aan verandering onderhevig.
Zie ook: http://unicode.org/history/publicationdates.html

Waarom is er Unicode?
Het is een oplossing voor een steeds terugkerend probleem met alle computers, PC's, Mac's, UNIX-en, en mainframes.
Karakters worden in karaktersets opgeslagen, maar niet alle karakters passen in een set. Dat komt omdat men zich beperkte tot 7 bits voor de opslag, wat ruimte biedt aan 128 unieke nummers (codepoints), die corresponderen met 128 unieke karakters.
Het had als gevolg dat voor elke set een keuze gemaakt moest worden. Welke karakters komen in welke volgorde in de set?
Er werden veel verschillende sets gemaakt, die uiteraard niet volledig compatible waren.
Het omzetten van de ene bronkarakterset naar de doelkarakterset (transcoding) is goed te doen als karakters in beide sets aanwezig zijn.
Dan hoeven alleen de codepoints omgenummerd te worden. Het grote probleem komt wanneer de benodigde karakters niet in de doelkarakterset aanwezig zijn. Dan treedt er verlies van data en dus meestal altijd verlies van betekenis op, waardoor data en ook programmatuur corrupt raakt.

Wat waren de problemen?
Computers waren beperkt, omdat bytes duur waren. Aanvankelijk waren er 7 bits beschikbaar: 7 data bits en 1 pariteitsbit. In 1986 werd de meest recente versie van (US-)ASCII ontwikkeld, een standaard van 128 karaketers om gegevensuitwisseling tussen verschillende computers mogelijk te maken. Het is nog steeds de standaard op UNIX- en PC-machines.
Naarmate er mondiaal meer gegevens digitaal uitgewisseld werden, nam de behoefte toe aan meer verschillende karakters.
Door de pariteitsbit te gebruiken als data bit konden nog eens 128 karakters worden opgeslagen, ook wel bekend als de Extended ASCII-set. Voor de laatste 128 karakters werden wederom vele verschillende subsets (codepages) bedacht, met dezelfde oude transcodingsproblematiek.

Wat zijn de problemen nu dan nog?
In de Westerse wereld wordt de codepage Latin1 (ISO/IEC-8859-1) als aanvulling op ASCII gebruikt.
Helaas bestaat daarover de nodige verwarring, omdat Microsoft een tweede, veelgebruikte Extended ASCII codepage (CP-1252) heeft ontwikkeld voor Latijnse talen. Deze wordt toegepast vanuit DOS/Windows op PC's, waardoor consistente gegevensuitwisseling met andere systemen (Apple, UNIX, mainframe) bemoeilijkt wordt.
Maar ook tussen andere bestaande karaktersets bestaat nog steeds hetzelfde probleem van gegevensverlies bij transcoding.
Dit komt -- niet toevallig -- het meest in zicht via het internet. Tussen allerlei talen worden gegevens uitgewisseld. Vroeger per fax, nu volledig digitaal. En de verschillende manieren waarop karakters worden opgeslagen maken het regelmatig tot een logische uitdaging.
Tegelijkertijd is het probleem ontzettend onzichtbaar. We zien alleen de beeldpunten van het scherm, of de inktpunten op papier die de karakters vormen. De onderliggende bits en bytes kunnen we alleen goed zien met aparte software als een HEX-editor.

En de oplossing is Unicode?
Door elk bestaand karakter een eigen codepoint te geven, kan er geen verwarring bestaan over welk karakter bedoeld wordt.
Er kan altijd naar Unicode getranscodeerd worden. Dat is de grootste waarde van Unicode.
Toch lost dat nog niet alle problemen op.
Een groot probleem met Unicode is echter dat het totaal aantal codepoints niet te vatten is in één enkele byte. Daarmee breekt het gebruik van Unicode met de meeste bestaande programmeertalen, waarin de aanname is gedaan dat 1 byte gelijkstaat aan 1 karakter. Dit is dusdanig backward incompatible dat het niet gelukt is PHP 6 van de grond te krijgen. Het probleem blijft niet bepert tot PHP, alle programmeertalen, ook die in databases hebben met encoding te maken en hebben hiervoor een eigen oplossing. PHP 7 voorziet weinig verbetering op dit vlak, zie ook de migration guide op: http://php.net/manual/en/migration70.php
Een kleiner probleem is dat lang niet alle karakters door de meeste lettertype ondersteund worden.

Maar hadden we voor het web niet al UTF-8?
Bij een Single Byte Character Set (SBCS) waarbij 1 byte gelijk staat aan 1 karakter, is de codepoint direct te vertalen naar karakter. Met Unicode is het concept van hoe de codepoints worden genummerd, losgeweekt van hoe de codepoints worden opgeslagen (encoding). Om Unicode op te slaan zijn er verschillende encodings ontwikkeld, de meeste bekende zijn: UTF-8, UTF-16 en UTF-32.
Maar er zijn er nog veel meer: UTF-1, UTF-7, UTF-EBCDIC, SCSU, BOCU, etc.
Alle Unicode encodings zijn een Multi Byte Character Set (MBCS), en sommige (waaronder UTF-8) zijn variabel in lengte (VMBCS). Met UTF-8 kan 1 karakter 1 tot en met 6 bytes in beslag nemen.
Voor het web (HTML5 en JavaScript) is gekozen voor UTF-8 als standaard-encoding.

Waarom UTF-8?
Het heeft als belangrijk voordeel dat ASCII volledig compatible is met Unicode. Beter nog: ASCII hoeft niet naar UTF-8 getranscodeerd, het is al een subset van UTF-8. Hierdoor worden eventuele problemen met data en programmatuur met de meestgebruike UNIX- en PC-systemen zoveel mogelijk vermeden.
Een tweede voordeel is dat UTF-8 de karakters opslaat in groepjes van losse bytes. UTF-8 is variabel in lengte, karakters worden in 1 byte t/m 6 bytes opgeslagen. Daarmee worden ook problemen tussen Little Endian en Big Endian systemen vermeden. Daardoor is ook een speciaal Unicode karakter, de Byte Order Mark (BOM), optioneel bij UTF-8, terwijl het vereist is bij andere MBCS-encodings. De enige noodzaak om een BOM te gebruiken is om tekst-editors te hinten dat de tekst als UTF-8 is opgeslagen.
Met UTF-8 wordt het voordeel van Unicode behouden, en transcoding tussen andere Unicode-encodings is geen probleem.

Technisch zijn er twee voordelen waarom UTF-8 de voorkeur heeft boven andere encodings:
Ten eerste heeft UTF-8 geen codepoint waarbij er in 1 byte alleen maar 8 nullen voorkomen. Dit speciale NUL-karakter geeft in veel programmeertalen het einde aan van een string, waarna de overige bytes genegeerd worden.
Ten tweede kan je vanaf elk willekeurige byte eenvoudig een vorig of volgend karakter kan aanwijzgen. Het is mogelijk omdat de eerste byte van een groepje altijd in de eerste bit een 0 bevat, of 11 in de eerste twee bits. Dat aspect wordt zelfsynchronisatie genoemd en is handig bij streams.

Hoe staat PHP ten opzichte van Unicode?
Er gaat een gerucht dat PHP 5 agnostisch is ten opzichte van encoding. Maar dat klopt niet helemaal: de encoding waarin .php-bestanden moeten worden opgeslagen moet (US-)ASCII compatible zijn.
Zie ook: http://php.net/manual/en/mbstring.php4.req.php
PHP gaat in de aanwezige functies voor de programmeur standaard uit van Latin1 (ISO/IEC-8859-1).
Er is beperkte ondersteuning voor UTF-8, met utf8_* -functies, en de mb_* functies.
De rest wordt aan de individuele programmeur overgelaten. PHP 5 is daarin niet uniek.
PHP 6 zou verandering brengen maar is helaas gestrand.
PHP 7 staat voor de deur, met enkele verbeteringen op het gebied van Unicode.
Pagina 3

PHP en UTF-8

Help, mijn PHP geeft vreemde tekens!
Met een zekere regelmaat komt die vraag naar voren.
De oorzaak is altijd dezelfde: daar waar transcoding tussen verschillende encodings had moeten plaatsvinden is dat niet gebeurd.
Onderliggende oorzaak is dat de programmeur niet het volledige zicht heeft op encoding.

Maar hoe los ik dat op?
Het is niet mogelijk om tekstuele data te interpreteren zonder de bijbehorende encoding (en dus karakterset) te weten.
Sla je bijvoorbeeld tekens op in UTF-8, en geef je verderop in het programma aan dat de bytes als Latin1 behandeld moeten worden, dan gaat het mis.
Het probleem van misinterpretatie beperkt zich niet tot encodings, het komt ook voor met andere datatypen. Bijvoorbeeld integers.
Als je niet weet hoeveel bits de integer is (8, 16, 32, 64, ..) en of het een signed of unsigned integer is, hoe kan je dan weten wat het getal is dat de bits bevatten?
Misinterpretatie van integers in PHP komt weinig voor. Misinterpretatie van encodings komt geregeld voor, en gelukkig is het gemakkelijk te voorkomen.
De oplossing? Sla alles op met UTF-8, en gebruik alleen PHP-functies die met VMBCS-encodings om kunnen gaan.

Waar moet ik kijken?
Data komt overal vandaan: in de browser wordt het automatisch getranscodeerd naar Unicode als je HTML-pagina dat ook is.
Je kunt externe gegevens lezen en schrijven via een databaseconnectie of een bestand, via de CLI, etc.
Data kan in PHP worden bewerkt, en PHP slaat het opnieuw op, stuurt het terug naar de browser.

Wat moet er dan allemaal in UTF-8?
Alles, of zoveel mogelijk. Want als alle data dezelfde encoding heeft, hoeft er niet getranscodeerd te worden.
Bijkomend voordeel: als een ontvangende partij geen Unicode snapt, ligt de beperking niet bij de PHP applicatie.

Bij alles moet je denken aan encoding van:
* Tekstbestanden (.php-bestanden, HTML-pagina's en databestanden)
* Databases (data en connectie)
* Browser (in- en uitvoer)
* Verwerking in PHP (instellingen, multibyte-functies)
Pagina 4

Tekstbestanden

PHP-programmabestanden
PHP-bestanden moeten ASCII-compatible worden opgeslagen. En ASCII is een subset van UTF-8.
Dus .php-bestanden kunnen veilig als UTF-8 (zonder BOM) worden opgeslagen.
Daarmee worden alle hardcoded strings en array-waarden ook automatisch UTF-8.

Wat gaat er wel eens fout:
<?php
$hi = 'Hallo cliënt!'; // Latin1 string
header('Content-type: text/plain; charset=utf-8'); // Browser: nu volgt UTF-8
print $hi; // Browser interpreteert Latin1 string als UTF-8, gaat fout
?>


Een onhandige fix is om encoding te vermijden via HTML:
<?php
print htmlentities($hi); // karakter wordt omgezet naar langere HTML-entiteit
?>


Een snelle fix:
<?php
print utf8_encode($hi); // karakter wordt eerst getranscodeerd naar UTF-8
?>


Structurele oplossing:
<?php
$hi = 'Hallo cliënt!'; // UTF-8 string, want .php is opgeslagen als UTF-8 zonder BOM
header('Content-type: text/plain; charset=utf-8'); // Browser: nu volgt UTF-8
print $hi; // Browser interpreteert UTF-8 string als UTF-8, geen probleem
?>


Hardcoded strings zijn op deze manier eenvoudig aan te passen door de .php-bestanden op te slaan als UTF-8. Merk op dat dit ook van invloed is op alle strings, dus ook SQL queries die in PHP zijn opgeslagen.

HTML-bestanden
HTML 5 hanteert standaard UTF-8 encoding, en ook JavaScript is Unicode (UTF-16). Er zijn drie plaatsen waar je aan de browser de encoding van de HTML door kunt geven: In de HTTP-header, in de XML-header bij XHTML, en als een metatag.

De laatste jaren wordt zoveel mogelijk in HTML5 gedaan. Hoewel die standaard nog niet definitief is hebben de meeste browsers de functionaliteit al wel geïmplementeerd. HTML5 is SGML en helaas niet compatible met XML, polyglot HTML kunnen we vergeten en de bekende XML-header valt af:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE html>


De charset-metatag is vooral handig bij een opgeslagen HTML5-bestand wanneer er geen andere indicatie is van de encoding.
In de /body/head node van een HTML5 document ziet het er vaak zo uit:
<meta charset="utf-8" />

Dit is de verkorte versie van:
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />

Wat hier te lezen valt is dat 'http-equiv' staat voor HTTP-equivalent, ofwel gelijkwaardig aan de HTTP-header. Dat is verwarrend als de HTTP-header zelf op een andere encoding is geconfigureerd, bijvoorbeeld wanneer een webserver autistisch is afgesteld om alles met Latin1 in de HTTP-header te serveren. Dan weet de browser in feite niet welke van de twee opgegeven encodings de juiste is.

Het beste is om alle encodings met de HTTP-header op te geven, althans voor alle HTML5 wat via de webserver aan de browser wordt aangeboden. Dan is er een single version of the truth en je geeft niet iets aan met HTML dat eigenlijk via de HTTP-header hoort te gaan, als volgt:
<?php
header('Content-Type: text/html; charset=utf-8'); /** @link http://www.w3.org/International/O-HTTP-charset */
readfile($sHTML5bestand);
?>


Databestanden
Bijvoorbeeld lezen of schrijven van databestanden van of naar CSV. Ook hier is encoding cruciaal.
Maak je gebruik van binaire functies als fopen() of fgetcsv() dan moet je alles zelf transcoden waar nodig.
Dat kan alleen als je weet welke bron- en doelencoding de bestanden hebben, het verschilt per versie van software en ook per platform.
Het moet duidelijk zijn uit de bijbehorende documentatie.
Pagina 5

Overige in- en uitvoer

Browserinvoer
Dit onderdeel gaat eigenlijk automatisch, omdat transcoding automatisch geregeld wordt. Is je HTML-pagina in UTF-8, dan zal de browser de gegevens in UTF-8 aanleveren. Een ander voorbeeld: als iemand op een Mac werkt en uit Office 2011 kopieert met de "Latin1"-encoding, dan transcodeert het automatisch naar Unicode bij het plakken. Omgekeerd werkt het ook, met bijvoorbeeld een copy paste vanuit de browser naar een tekstverwerker.

Is de data eenmaal opgestuurd en aangekomen bij PHP in een globale variabele als $_GET, $_POST of $_REQUEST, dan moet er slechts één ding gebeuren. Alle informatie opgestuurd vanuit de browser moet als tainted (aangetast) worden beschouwd, en voordat we er iets mee doen moet de data gevalideerd worden. Immers, niet elk groepje van bytes levert een valide codepoint op, er kan bewust mee geknoeid zijn. Een groepje bytes kan bewust
extra lang worden gemaakt met veel meer dan 6 bytes, om te hacken, of bugs te vinden, of meer algemeen om de applicatie om zeep te helpen.

Een goede manier om de validiteit te controleren is met de reguliere expressie die door het W3C wordt aanbevolen:
<?php
function is_utf8($sData){
  return preg_match('%^(?:
      [\x09\x0A\x0D\x20-\x7E]            # ASCII
    | [\xC2-\xDF][\x80-\xBF]             # non-overlong 2-byte
    | \xE0[\xA0-\xBF][\x80-\xBF]         # excluding overlongs
    | [\xE1-\xEC\xEE\xEF][\x80-\xBF]{2}  # straight 3-byte
    | \xED[\x80-\x9F][\x80-\xBF]         # excluding surrogates
    | \xF0[\x90-\xBF][\x80-\xBF]{2}      # planes 1-3
    | [\xF1-\xF3][\x80-\xBF]{3}          # planes 4-15
    | \xF4[\x80-\x8F][\x80-\xBF]{2}      # plane 16
  )*$%xs', $sData);
}
?>


Browseruitvoer
De browser moet weten dat wat het ontvangt UTF-8 is, dat doen we door eenduidig de encoding te vermelden met de HTTP-header 'Content-Type: text/html; charset=utf-8'. Vervolgens moet de programmeur er voor zorgen dat de output daadwerkelijk UTF-8 is. Dat klinkt eenvoudiger dan het is, getuige het volgende hoofdstuk.
Pagina 6

Databases

Gegevens
Ook databases hebben bij alle tekstuele data een encoding nodig om te weten wat de data betekent, en hoe te transcoden.
Het meest praktisch is om al deze data ook in UTF-8 te houden:
- standaardencoding van de database en eventueel het schema
- standaardencoding (eventueel geërfd van de database) van de tabellen
- standaardencoding (eventueel geërfd van de tabel) van de kolommen

Meestal wordt PHP gecombineerd met MySQL aangeboden, en soms is MySQL vervangen door de fork MariaDB.
Beide databases kennen de twee encodings "utf8" en "utf8mb4". Alleen "utf8mb4" ondersteunt de volledig Unicode set, "utf8" ondersteunt ruim 1 miljoen codepoints minder. MySQL ondersteunt "utf8mb4" vanaf versie 5.5.3 uit 2010, we gaan er van uit dat de meesten hierover beschikken.
Zie ook: https://dev.mysql.com/doc/relnotes/mysql/5.5/en/news-5-5-3.html

Omzetten
Bij een bestaande applicatie is het handig om ook de data om te zetten naar UTF-8. Dat kan een hele klus zijn afhankelijk van hoe groot en complex de database is. Hoe het precies moet verschilt per database.

Voor MySQL:
A. maak een backup van de database
B. stel de encoding in van de database met
ALTER DATABASE <databasenaam> CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci;

C. stel de encoding in van alle tabellen met
ALTER TABLE <tabelnaam> CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

D. pas waar nodig de kolommen aan op dezelfde manier:
ALTER TABLE <tabelnaam> CHANGE <kolomnaam> <kolomnaam> <definitie> CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

E. check of de lengte van kolommen en indices nog volstaat
In tegenstelling tot andere databases als PostgreSQL is de grootte in MySQL aangegeven met het aantal bytes, en dus niet het aantal karakters. Een kolom van het type VARCHAR(255) biedt in het beste geval ruimte aan 255 karakters, maar in het slechtste geval 42 karakters.
F. doe een REPAIR en OPTIMIZE voor elke tabel, of gebruik hiervoer de CLI-tool mysqlcheck met de opties --auto-repair --optimize.

Verbinding
Vanuit PHP gebruik je een extentie om de database server te bereiken. De extentie is doorgaans een API-wrapper om de client library van de database server. Vanuit PHP heb je dan eigenlijk een database client, met de losse eindjes (functies) in PHP waarvan je zelf mag weten hoe je ze gebruikt. Met deze client maak je een verbinding naar de database server, en nu komt het: die heeft een encoding van zichzelf. Zowel de database server als de client library zullen alle tekstuele data automatisch transcoden naar de encoding van de verbinding.

Omdat we transcoding het liefst helemaal vermijden moet ook de encoding van de verbinding worden ingesteld op UTF-8.
Dat doen we direct bij of net na de totstandkoming van de verbinding.
Hoe dat precies moet hangt af van de extentie in PHP (de API naar de client library)

MySQLi voorbeeld:
<?php
$db = new mysqli($host, $account, $password, $database);
$db->set_charset('utf8mb4');
?>


PostgreSQL voorbeeld:
<?php
$db = pg_connect("host=localhost options='--client_encoding=UTF8'");
?>


PDO voorbeeld met PostgreSQL:
<?php
$db = new PDO("pgsql:host=<host>;dbname=<database>;options='--client_encoding=UTF8'");
?>


Is de encoding eenmaal ingesteld dan hoeven we verder niet langer na te denken en zal de data zonder encoding-problemen worden geschreven en gelezen.
Zie ook: http://php.net/manual/en/refs.database.php
Pagina 7

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:
<?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:
<?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.
<?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:
<?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:
<?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:
<?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:
<?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.
Pagina 8

Conclusie

Unicode is een must voor het web en dus voor websites en -applicaties. Tenzij de keuze is om een applicatie bewust in Latin1 door te ontwikkelen. Je weet echter nooit hoe je software zal groeien. Hoe groter en internationaler de applicatie of website, hoe groter de kans dat Latin1 niet genoeg zal zijn en uiteindelijk Unicode nodig is. Hoe langer gewacht wordt met de overstap, des te meer tijd zal het kosten om de encoding aan te passen. We worden gestimuleerd om er over na te denken door veranderende standaardinstellingen van PHP richting Unicode.

Voor iedere PHP-er wordt hierdoor bewustzijn van encoding belangrijk.

Reacties

0
Nog geen reacties.