Signature
mb_scrub(string $string, ?string $encoding = null): stringPurpose
mb_scrub() cleans a string that may contain byte sequences that are not valid in a given character encoding. It converts the string from that encoding to the same encoding, and in doing so every ill-formed sequence is swapped for the current substitute character. The result is a string you can safely pass to code that insists on well-formed text, such as a regular expression with the u modifier.
The function does not guess what the original bytes meant. It does not repair a Latin-1 string that was mislabelled as UTF-8, and it does not transcode anything. It only removes the invalid parts so that the remaining string is valid.
Signature
Parameters and defaults
$string: the input to clean. It may be valid or invalid, and an empty string is fine.$encoding: the encoding used to interpret the bytes, for example'UTF-8'. When it is omitted ornull, PHP uses thembstring.internal_encodingsetting if one is set, and otherwise thedefault_charsetsetting. Passing the encoding explicitly makes your code independent of configuration, which is usually a good idea.
Return value and failure cases
The function returns a string. A string that was already valid comes back unchanged. Invalid sequences are replaced according to the substitute character configured with mb_substitute_character(). The usual default is ?, but you can switch to U+FFFD, which is encoded in UTF-8 as the bytes EF BF BD.
Since PHP 8.0, an unknown encoding name throws a ValueError. Before PHP 8.0 a warning was raised instead. There is no false return in PHP 8.4, so you do not need to check for it.
Example 1: seeing the substitution at byte level
Terminals and browsers may render broken bytes with their own replacement glyph, so bin2hex() is the reliable way to see what a string contains. The byte 0xFF can never occur in UTF-8.
mb_substitute_character(0x3F);
$input = "A\xFFB";
echo bin2hex($input), "\n";
echo bin2hex(mb_scrub($input, 'UTF-8')), "\n";
mb_substitute_character(0xFFFD);
echo bin2hex(mb_scrub($input, 'UTF-8')), "\n";41ff42 413f42 41efbfbd42
The first line shows the raw input, the second shows ? (3f) in place of the bad byte, and the third shows the three-byte encoding of U+FFFD.
Example 2: preparing input for a Unicode regex
A PCRE pattern with the u modifier fails on malformed UTF-8. Scrubbing first lets the match succeed.
mb_substitute_character(0x3F);
$input = "A\xFFB";
var_dump(preg_match_all('/./us', $input));
echo preg_last_error_msg(), "\n";
$clean = mb_scrub($input, 'UTF-8');
var_dump(preg_match_all('/./us', $clean));bool(false) Malformed UTF-8 characters, possibly incorrectly encoded int(3)
The first call returns false and the error message explains why. After scrubbing, the string is A?B and three characters are matched.
Example 3: valid characters survive
Only the broken part is replaced. Valid multibyte characters such as é (bytes C3 A9) are kept exactly as they were, and mb_check_encoding() confirms the string is valid afterwards.
mb_substitute_character(0x3F);
$input = "caf\xC3\xA9\xFF!";
$clean = mb_scrub($input, 'UTF-8');
echo json_encode([
'before' => mb_check_encoding($input, 'UTF-8'),
'after' => $clean,
'valid' => mb_check_encoding($clean, 'UTF-8'),
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), "\n";{"before":false,"after":"café?!","valid":true}
Edge cases
- Valid input is returned unchanged, so calling
mb_scrub()on clean text is safe, though it costs a pass over the string. - The substitute character is global state for the mbstring extension. If other code changes it with
mb_substitute_character(), your output changes too. Set it explicitly where the exact result matters. - The
?default can be confusing, because a literal question mark in the original text is indistinguishable from a substituted byte. Use U+FFFD when you want the damage to be recognisable. - A truncated multibyte sequence, such as the first byte of a two-byte character at the end of a string, is ill-formed and gets replaced as well.
- If the string is really in another encoding such as ISO-8859-1, scrubbing it as UTF-8 destroys its accented letters. Convert it with
mb_convert_encoding()instead.
Common mistakes
- Treating it as a converter.
mb_scrub()keeps the encoding the same. It cannot turn legacy bytes into proper UTF-8 text. - Relying on the default encoding. A different
default_charseton another server can change how the bytes are read. Pass'UTF-8'explicitly. - Scrubbing instead of validating. If invalid input should be rejected, for example in a strict API, check it with
mb_check_encoding()and report an error rather than silently altering data. - Expecting a
falsereturn. Since PHP 8.0 a bad encoding name raises aValueError, so catch that exception if the encoding name comes from user input. - Inspecting results visually. Rendering may hide invalid bytes. Use
bin2hex()ormb_check_encoding()when debugging.
Related functions
mb_substitute_character()gets or sets the character used for replacements.mb_check_encoding()tells you whether a string is valid for an encoding.mb_convert_encoding()converts text between different encodings.
Recap
mb_scrub() takes a string and an optional encoding, replaces every ill-formed byte sequence with the current substitute character and returns a valid string. Use it to sanitise untrusted text before Unicode-aware processing, pass the encoding explicitly, and remember that it removes damage rather than repairing meaning.
Tested examples
mb_substitute_character(0x3F);
$input = "A\xFFB";
echo bin2hex($input), "\n";
echo bin2hex(mb_scrub($input, 'UTF-8')), "\n";
mb_substitute_character(0xFFFD);
echo bin2hex(mb_scrub($input, 'UTF-8')), "\n";41ff42 413f42 41efbfbd42
mb_substitute_character(0x3F);
$input = "A\xFFB";
var_dump(preg_match_all('/./us', $input));
echo preg_last_error_msg(), "\n";
$clean = mb_scrub($input, 'UTF-8');
var_dump(preg_match_all('/./us', $clean));bool(false) Malformed UTF-8 characters, possibly incorrectly encoded int(3)
mb_substitute_character(0x3F);
$input = "caf\xC3\xA9\xFF!";
$clean = mb_scrub($input, 'UTF-8');
echo json_encode([
'before' => mb_check_encoding($input, 'UTF-8'),
'after' => $clean,
'valid' => mb_check_encoding($clean, 'UTF-8'),
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), "\n";{"before":false,"after":"café?!","valid":true}