Multibyte & Unicode Strings
Earlier pages establish that a PHP string is a sequence of bytes, not a sequence of "characters." That design choice is invisible as long as text stays within plain ASCII, where one character always occupies exactly one byte. The moment text contains accented letters, non-Latin scripts, or emoji encoded as UTF-8, byte-oriented functions like strlen() and substr() start returning answers that don't match what a human sees on screen. This page explains why that happens and how PHP's mbstring extension fixes it.
Why UTF-8 breaks byte-based functions
UTF-8 encodes each character using between one and four bytes. Plain ASCII letters use one byte, but an accented letter like é or an emoji like 🚀 is encoded using multiple bytes glued together to represent a single visible character. strlen() has no concept of "one character" — it just counts bytes — so it reports a length far larger than the number of characters a person would count.
broken-strlen.php
<?php $word = 'café'; // é is a multibyte UTF-8 character $emoji = '🚀'; echo strlen($word) . PHP_EOL; // bytes, not 4 characters echo strlen($emoji) . PHP_EOL; // bytes, not 1 character
5 4
café looks like four characters but reports as five bytes, because é alone takes two bytes in UTF-8. The rocket emoji looks like one character but takes four bytes. Both numbers are byte counts, which is exactly what strlen() promises — but it's rarely what you actually wanted to know.
substr() slicing mid-character
The same byte-vs-character mismatch is worse with substr(), because it can slice a multibyte sequence right down the middle, producing a fragment of invalid UTF-8 that renders as garbled text or a replacement-character symbol instead of the letter you wanted.
broken-substr.php
<?php $word = 'café'; // Intending to grab the first 3 "characters" $slice = substr($word, 0, 3); var_dump($slice); echo mb_check_encoding($slice, 'UTF-8') ? 'valid UTF-8' : 'BROKEN UTF-8';
string(3) "caf" BROKEN UTF-8
The mbstring extension
PHP's mbstring extension provides character-aware equivalents of the standard string functions, prefixed with mb_. mb_strlen() counts actual characters (technically, encoded code points) instead of bytes, and mb_substr() slices on character boundaries so it never splits a multibyte sequence.
mbstring-fixed.php
<?php $word = 'café'; $emoji = '🚀 launch'; echo mb_strlen($word) . PHP_EOL; // 4 — actual character count echo mb_strlen($emoji) . PHP_EOL; // 8 — rocket + space + "launch" echo mb_substr($word, 0, 3) . PHP_EOL; // 'caf' still, but safely echo mb_substr($word, 0, 4); // 'café' — full word, correct
4 8 caf café
mb_strtoupper() and locale-aware case conversion
strtoupper() only understands plain ASCII, so it leaves accented letters completely unchanged. mb_strtoupper() (and its lowercase counterpart mb_strtolower()) correctly uppercase or lowercase characters outside the ASCII range, as long as you specify or have configured the right encoding.
mb-case-conversion.php
<?php $city = 'québec'; echo strtoupper($city) . PHP_EOL; // é is left alone echo mb_strtoupper($city) . PHP_EOL; // é becomes É correctly
QUéBEC QUÉBEC
Setting the internal encoding
Most mb_* functions accept an optional encoding argument, but passing it every single call is repetitive. mb_internal_encoding() lets you set a default encoding once — near the top of a script or in a bootstrap file — so every subsequent mb_* call assumes UTF-8 (or whatever encoding your application uses) automatically.
internal-encoding.php
<?php
mb_internal_encoding('UTF-8');
$name = 'Zoë';
echo mb_strlen($name) . PHP_EOL;
echo mb_strtoupper($name);3 ZOË
Side-by-side: broken vs fixed
Putting the byte-based and multibyte-aware functions next to each other on the same input makes the difference concrete — this is the exact comparison worth running whenever a "string looks cut off weirdly" bug report involves non-English names or emoji.
side-by-side.php
<?php $name = 'Renée 🎉'; echo 'strlen: ' . strlen($name) . PHP_EOL; echo 'mb_strlen: ' . mb_strlen($name) . PHP_EOL; echo 'substr: ' . substr($name, 0, 6) . PHP_EOL; echo 'mb_substr: ' . mb_substr($name, 0, 6) . PHP_EOL;
strlen: 11 mb_strlen: 7 substr: Renée mb_substr: Renée 🎉
Byte-based | Multibyte-aware | Difference |
|---|---|---|
strlen() | mb_strlen() | Bytes vs actual character count |
substr() | mb_substr() | May split a character vs always slices on character boundaries |
strtoupper() / strtolower() | mb_strtoupper() / mb_strtolower() | ASCII-only vs full Unicode case conversion |
strpos() | mb_strpos() | Byte offset vs character offset |
UTF-8 encodes many characters using more than one byte, so byte-counting functions overcount length.
substr() can slice inside a multibyte character, producing invalid UTF-8 output.
The mbstring extension provides mb_ prefixed equivalents that operate on characters, not bytes.
mb_internal_encoding() sets a script-wide default so you don’t repeat the encoding argument everywhere.
Always verify mbstring is enabled in the target environment before relying on it in production.