Multibyte Strings and Unicode

The web is multilingual: names like "José" and "Zoë", text in Greek, Hindi or Japanese, and emoji everywhere. PHP strings are sequences of bytes, while UTF-8 uses between one and four bytes per character, so byte-based functions can count, cut and change case incorrectly. The mbstring extension's mb_* functions work with characters instead.

Bytes versus characters

PHP
<?php

$words = ["cat", "café", "Ελλάδα", "日本", "👍"];

foreach ($words as $w) {
    printf("%-12s bytes: %2d  characters: %d\n", $w, strlen($w), mb_strlen($w));
}
Output
cat          bytes:  3  characters: 3
café        bytes:  5  characters: 4
Ελλάδα bytes: 12  characters: 6
日本       bytes:  6  characters: 2
👍         bytes:  4  characters: 1

Notice that even the alignment is off: printf's width counts bytes, so multibyte words get less padding. ASCII characters take one byte each in UTF-8, which is why English-only code often seems to work fine — until the first customer named Zoë signs up.

Cutting strings safely

substr can cut a character in half, leaving invalid UTF-8 that displays as a "�". mb_substr counts characters:

PHP
<?php

$name = "Renée Dubois";

echo substr($name, 0, 4), "|\n";
echo mb_substr($name, 0, 4), "|\n";
echo mb_substr($name, -6), "\n";
var_dump(mb_check_encoding(substr($name, 0, 4), 'UTF-8'));
Output
Ren�|
René|
Dubois
bool(false)

mb_check_encoding tells you whether a string is valid UTF-8 — worth checking on input from untrusted sources.

Changing case

strtoupper and ucfirst only handle ASCII letters. The multibyte versions understand every alphabet:

PHP
<?php

$text = "élan vital in São Paulo";

echo strtoupper($text), "\n";
echo mb_strtoupper($text), "\n";
echo mb_convert_case($text, MB_CASE_TITLE), "\n";
echo mb_strtolower("ΑΘΗΝΑ"), "\n";
echo mb_ucfirst("über"), "\n";
Output
éLAN VITAL IN SãO PAULO
ÉLAN VITAL IN SÃO PAULO
Élan Vital In São Paulo
αθηνα
Über

mb_ucfirst and mb_lcfirst were added in PHP 8.4, along with mb_trim, mb_ltrim and mb_rtrim.

Searching and splitting

Most string functions that deal with positions have multibyte counterparts: mb_strpos, mb_stripos, mb_strrpos, mb_substr_count. To split a string into characters, use mb_str_split:

PHP
<?php

$word = "naïve";

echo strpos($word, "v"), " vs ", mb_strpos($word, "v"), "\n";
echo implode(" | ", mb_str_split($word)), "\n";
echo implode("", array_reverse(mb_str_split("ñandú"))), "\n";
echo strrev("ñandú"), "\n";     // byte reversal breaks characters
Output
4 vs 3
n | a | ï | v | e
údnañ
��dna��

Padding and trimming

PHP
<?php

$names = ["Ana", "Zoë", "Łukasz"];

foreach ($names as $n) {
    echo "[", mb_str_pad($n, 8, "."), "]\n";
}
echo "[", mb_trim("\u{3000}全角スペース\u{3000}"), "]\n";   // trims Unicode whitespace too
Output
[Ana.....]
[Zoë.....]
[Łukasz..]
[全角スペース]

mb_str_pad arrived in PHP 8.3. Use it whenever you align columns of text that might contain non-ASCII characters.

Characters and code points

mb_ord and mb_chr convert between a character and its Unicode code point, and the \u{…} escape writes any character into a double-quoted string:

PHP
<?php

echo mb_ord("€"), "\n";
echo mb_chr(9731), "\n";
echo "\u{1F680} launch\n";
printf("U+%04X\n", mb_ord("é"));
Output
8364
☃
🚀 launch
U+00E9

Encodings

Use UTF-8 everywhere: save your source files as UTF-8, send Content-Type: text/html; charset=UTF-8 (PHP does this by default), set <meta charset="utf-8"> in HTML, and use utf8mb4 as the character set in MySQL. When you receive text in another encoding, convert it at the boundary:

PHP
<?php

$latin1 = "\xE9t\xE9";        // "été" encoded as ISO-8859-1
var_dump(mb_check_encoding($latin1, 'UTF-8'));

$utf8 = mb_convert_encoding($latin1, 'UTF-8', 'ISO-8859-1');
echo $utf8, "\n";
echo strlen($latin1), " bytes became ", strlen($utf8), " bytes\n";
Output
bool(false)
été
3 bytes became 5 bytes

Byte or multibyte?

  • Use mb_* functions for anything a human might type or read: names, titles, comments, messages.
  • Byte functions are fine (and faster) for things that are guaranteed ASCII: hex hashes, identifiers, HTTP header names, protocol data.
  • Functions that don't depend on character boundaries — str_contains, str_replace, explode, implode — work correctly on UTF-8 as they are.

Try it yourself

Write a function that shortens text to a number of characters and adds an ellipsis, without breaking multibyte characters.

Try it yourself · main.php
Edit the code, copy it into main.php, then run it with php main.php (PHP 8.5). How to install PHP
Output
Crème brûlée i…
日本語のテ…
Short