Multibyte strings7 min readExamples run on PHP 8.5.10

mb_scrub()

Learn how mb_scrub() replaces invalid bytes in a string with the substitute character, with PHP 8.4 examples, edge cases and common mistakes.

Signature

mb_scrub(string $string, ?string $encoding = null): string
Available since PHP 7.2.07.48.08.18.28.38.48.5All versions →

Purpose

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 or null, PHP uses the mbstring.internal_encoding setting if one is set, and otherwise the default_charset setting. 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.

Example
<?php
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";
Output
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.

Example
<?php
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));
Output
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.

Example
<?php
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";
Output
{"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

  1. Treating it as a converter. mb_scrub() keeps the encoding the same. It cannot turn legacy bytes into proper UTF-8 text.
  2. Relying on the default encoding. A different default_charset on another server can change how the bytes are read. Pass 'UTF-8' explicitly.
  3. 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.
  4. Expecting a false return. Since PHP 8.0 a bad encoding name raises a ValueError, so catch that exception if the encoding name comes from user input.
  5. Inspecting results visually. Rendering may hide invalid bytes. Use bin2hex() or mb_check_encoding() when debugging.
  • 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

Byte-level view of the substitute character
<?php
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";
Output
41ff42
413f42
41efbfbd42
Scrubbing before a Unicode regular expression
<?php
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));
Output
bool(false)
Malformed UTF-8 characters, possibly incorrectly encoded
int(3)
Valid characters are preserved
<?php
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";
Output
{"before":false,"after":"café?!","valid":true}