Signature
mb_substr(string $string, int $start, ?int $length = null, ?string $encoding = null): stringPurpose
mb_substr() returns part of a string, counted in characters rather than bytes. It is the multibyte-aware counterpart of substr(). In UTF-8 a single character such as é or 日 can use several bytes, so a byte-based slice can cut a character in half and leave you with invalid text. mb_substr() counts whole characters in the given encoding, so a slice always starts and ends on a character boundary.
The first character is at position 0, the second at position 1, and so on.
Signature
Parameters and defaults
$string: the text to take a piece from.$start: where the slice begins. A non-negative value counts from the beginning, starting at zero. A negative value counts back from the end of the string.$length: the maximum number of characters to return. If you omit it or passnull, everything up to the end of the string is returned. The default isnull.$encoding: the character encoding, such as'UTF-8'. If you omit it or passnull, PHP uses the internal encoding. Passing it explicitly makes your code independent of that setting, so the examples here do so.
Return value and failure cases
The function always returns a string. If the requested range is empty, for example because $start is past the end of the text, you get an empty string rather than false.
Since PHP 8.0.0 an invalid $encoding name throws a ValueError. Before 8.0.0 it emitted a warning instead. In the same release the $encoding parameter became nullable.
The manual also notes a change in 8.3.2: for invalid strings (those with encoding errors), character indices are interpreted the same way as in most other mbstring functions. Even so, the safest approach is to validate untrusted input first, for example with mb_check_encoding().
Example 1: start, length and negative start
The string below contains accented letters. Each slice is measured in characters, so the results are what a reader would expect.
$text = "naïve café";
echo json_encode([
mb_substr($text, 0, 5, 'UTF-8'),
mb_substr($text, 6, null, 'UTF-8'),
mb_substr($text, -4, 2, 'UTF-8'),
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
echo "\n";["naïve","café","ca"]
The first call takes five characters, naïve. The second starts at character 6 and runs to the end, giving café. The third starts four characters from the end and takes two, giving ca.
Example 2: why not substr()
With substr() the count is in bytes. In UTF-8 each of these Japanese characters uses three bytes, so taking two bytes cuts the first character apart. The check below shows that the byte slice is no longer valid UTF-8, while mb_substr() returns two intact characters.
$text = "日本語";
$byteSlice = substr($text, 0, 2);
echo json_encode([
'mb' => mb_substr($text, 0, 2, 'UTF-8'),
'byteSliceValid' => mb_check_encoding($byteSlice, 'UTF-8'),
'byteLength' => strlen($byteSlice),
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
echo "\n";{"mb":"日本","byteSliceValid":false,"byteLength":2}
Example 3: a preview helper and edge cases
A common use is shortening text for a preview. The helper keeps one character of the limit for the ellipsis. The last two values in the result show an out-of-range start and a negative length.
function preview(string $s, int $max): string
{
if (mb_strlen($s, 'UTF-8') <= $max) {
return $s;
}
return mb_substr($s, 0, $max - 1, 'UTF-8') . '…';
}
echo json_encode([
preview("Crème brûlée", 6),
preview("Hi", 6),
mb_substr("abc", 5, 2, 'UTF-8'),
mb_substr("abcdef", 1, -2, 'UTF-8'),
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
echo "\n";["Crème…","Hi","","bcd"]
A negative $length leaves out that many characters from the end. In the last call, the slice starts at index 1 and stops two characters before the end, so it returns bcd.
Edge cases
- A
$startbeyond the end of the string returns"". - A
$lengthlarger than what remains simply returns what is left. - A
$lengthof0returns an empty string. - A negative
$startlarger than the string length is treated as the beginning of the string. - Characters are not always what users perceive as a single symbol. An emoji with modifiers or a letter plus a combining accent is several code points, and
mb_substr()counts code points, so it can still separate them. For such text, consider normalizing it first or using the intl extension's grapheme functions where available.
Common mistakes
- Using
substr()on UTF-8 text. It can split a character and produce garbled output or breakjson_encode(). - Relying on the internal encoding. If the default differs from your data, the counts are wrong. Pass the encoding explicitly.
- Mixing byte and character offsets. A position from
strpos()is a byte offset. Pairmb_substr()withmb_strpos()andmb_strlen()so all measurements use the same unit. - Passing a misspelled encoding name. In PHP 8 this throws a
ValueError, so do not expect afalsereturn. - Expecting a byte limit. If a database column or protocol limits bytes, use
mb_strcut()instead, which cuts by bytes without splitting characters.
Related functions
mb_strlen()counts characters.mb_strpos()finds a character position.mb_strcut()cuts by byte count while respecting character boundaries.mb_internal_encoding()gets or sets the default encoding.substr()is the byte-based version for binary or ASCII-only data.
Recap
mb_substr() slices by characters, supports negative start and length values, returns an empty string for empty ranges, and throws a ValueError for an unknown encoding. Pass 'UTF-8' explicitly and keep every length and position in the same unit.
Tested examples
$text = "naïve café";
echo json_encode([
mb_substr($text, 0, 5, 'UTF-8'),
mb_substr($text, 6, null, 'UTF-8'),
mb_substr($text, -4, 2, 'UTF-8'),
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
echo "\n";
["naïve","café","ca"]
$text = "日本語";
$byteSlice = substr($text, 0, 2);
echo json_encode([
'mb' => mb_substr($text, 0, 2, 'UTF-8'),
'byteSliceValid' => mb_check_encoding($byteSlice, 'UTF-8'),
'byteLength' => strlen($byteSlice),
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
echo "\n";
{"mb":"日本","byteSliceValid":false,"byteLength":2}
function preview(string $s, int $max): string
{
if (mb_strlen($s, 'UTF-8') <= $max) {
return $s;
}
return mb_substr($s, 0, $max - 1, 'UTF-8') . '…';
}
echo json_encode([
preview("Crème brûlée", 6),
preview("Hi", 6),
mb_substr("abc", 5, 2, 'UTF-8'),
mb_substr("abcdef", 1, -2, 'UTF-8'),
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
echo "\n";
["Crème…","Hi","","bcd"]