Multibyte strings7 min readExamples run on PHP 8.5.10

mb_strcut()

Learn how mb_strcut() slices a string by byte offsets while keeping multibyte characters intact, with PHP 8.4 examples, edge cases and common mistakes.

Signature

mb_strcut(string $string, int $start, ?int $length = null, ?string $encoding = null): string
Available since PHP 4.0.67.48.08.18.28.38.48.5All versions →

Purpose

mb_strcut() extracts part of a string using byte offsets and a byte length, but it never leaves a half-written multibyte character at the edges. If a boundary falls in the middle of a character, the cut is moved back to the first byte of that character. This is the main difference from substr(), which cuts at the exact byte and can produce malformed text. It also differs from mb_substr(), which counts characters rather than bytes.

The function is most useful when you have a hard byte budget, such as a fixed-width storage column, a protocol field or a header limit, and you still want valid text.

Signature

Parameters and defaults

  • $string: the string to cut.
  • $start: a byte position counted from zero. A negative value counts back from the end of the string. If its magnitude is larger than the string length, the result starts at the beginning.
  • $length: the length in bytes. If it is omitted or null, everything to the end is returned. A negative length ends the result that many bytes before the end of the string. If a negative length reaches back past the start position, the result is an empty string.
  • $encoding: the character encoding. If it is omitted or null, the internal encoding is used. Passing the encoding explicitly, such as 'UTF-8', makes the code independent of that setting.

Return value and failure cases

The function returns a string: the selected portion of the input. The result can be shorter than you asked for, and it can be empty when the first character at the cut point does not fit in the byte range.

Since PHP 8.0, an invalid $encoding name throws a ValueError. Before PHP 8.0 this produced a warning. The PHP 8.4.0 changelog notes that behavior is more consistent for invalid UTF-8 and UTF-16 input, so do not rely on particular output for malformed strings. Validate untrusted input first, for example with mb_check_encoding().

Example 1: boundaries move back to a character start

In 'aé€b' the letter a uses one byte, é uses two, € uses three and b uses one, for seven bytes in total.

Example
<?php
$text = 'aé€b';
echo json_encode([
    'bytes' => strlen($text),
    'three' => mb_strcut($text, 0, 3, 'UTF-8'),
    'five' => mb_strcut($text, 0, 5, 'UTF-8'),
    'six' => mb_strcut($text, 0, 6, 'UTF-8'),
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
Output
{"bytes":7,"three":"aé","five":"aé","six":"aé€"}

A limit of 5 bytes would end inside the three-byte €, so the euro sign is left out. With 6 bytes it fits.

Example 2: comparing substr() and mb_strcut()

Example
<?php
$text = '€uro';
$plain = substr($text, 0, 2);
$safe = mb_strcut($text, 0, 2, 'UTF-8');
echo json_encode([
    'substrHex' => bin2hex($plain),
    'substrValid' => mb_check_encoding($plain, 'UTF-8'),
    'strcutHex' => bin2hex($safe),
    'strcutValid' => mb_check_encoding($safe, 'UTF-8'),
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
Output
{"substrHex":"e282","substrValid":false,"strcutHex":"","strcutValid":true}

substr() returns the first two bytes of the euro sign, which is not valid UTF-8. mb_strcut() returns an empty string, which is valid.

Example 3: fitting text into a byte limit

Example
<?php
function fitBytes(string $text, int $maxBytes): string
{
    return mb_strcut($text, 0, $maxBytes, 'UTF-8');
}

$results = [];
foreach ([1, 2, 6, 7] as $limit) {
    $results[] = fitBytes('ñandú', $limit);
}
echo json_encode($results, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
Output
["","ñ","ñand","ñandú"]

The word ñandú takes seven bytes because ñ and ú use two bytes each. A limit of 1 cannot hold ñ, so the result is empty. A limit of 6 cannot hold the final ú, so the result stops after ñand.

Edge cases

  • The result may contain fewer bytes than $length. Never assume strlen() of the result equals the requested length.
  • Because the unit is bytes, a given $length can hold different numbers of characters depending on the text.
  • A start offset that lands inside a character is moved back to that character's first byte.
  • With a single-byte encoding or plain ASCII text, every byte is a whole character, so the function behaves like substr().
  • A start offset beyond the end of the string gives an empty string.

Common mistakes

  1. Expecting character counts. If you want to count characters, use mb_substr(). mb_strcut() is for byte budgets.
  2. Using substr() for limits on UTF-8 text. This can split a character and produce invalid output that later breaks JSON encoding or display.
  3. Forgetting the encoding. If the internal encoding differs from your data, boundaries are computed wrongly. Pass 'UTF-8' explicitly when you know the encoding.
  4. Assuming the result is padded or exact. If you need a fixed-width field, pad the result yourself afterwards.
  5. Passing untrusted, unvalidated bytes. Output for malformed input is not something to depend on, so check it first.
  • mb_substr() extracts by character count.
  • mb_strlen() counts characters, while strlen() counts bytes.
  • mb_strimwidth() truncates by display width.
  • mb_internal_encoding() gets or sets the default encoding.
  • mb_check_encoding() validates a string for an encoding.

Recap

mb_strcut() takes byte-based $start and $length values and moves any cut that falls inside a multibyte character back to the character's first byte. It returns a valid but possibly shorter string, throws ValueError for an unknown encoding, and is the right choice when a byte limit must be respected without corrupting text.

Tested examples

Boundaries move back to a character start
<?php
$text = 'aé€b';
echo json_encode([
    'bytes' => strlen($text),
    'three' => mb_strcut($text, 0, 3, 'UTF-8'),
    'five' => mb_strcut($text, 0, 5, 'UTF-8'),
    'six' => mb_strcut($text, 0, 6, 'UTF-8'),
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
Output
{"bytes":7,"three":"aé","five":"aé","six":"aé€"}
Comparing substr() and mb_strcut()
<?php
$text = '€uro';
$plain = substr($text, 0, 2);
$safe = mb_strcut($text, 0, 2, 'UTF-8');
echo json_encode([
    'substrHex' => bin2hex($plain),
    'substrValid' => mb_check_encoding($plain, 'UTF-8'),
    'strcutHex' => bin2hex($safe),
    'strcutValid' => mb_check_encoding($safe, 'UTF-8'),
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
Output
{"substrHex":"e282","substrValid":false,"strcutHex":"","strcutValid":true}
Fitting text into a byte limit
<?php
function fitBytes(string $text, int $maxBytes): string
{
    return mb_strcut($text, 0, $maxBytes, 'UTF-8');
}

$results = [];
foreach ([1, 2, 6, 7] as $limit) {
    $results[] = fitBytes('ñandú', $limit);
}
echo json_encode($results, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
Output
["","ñ","ñand","ñandú"]