Apps often hide part of a phone number, email address or ID before logging or displaying it. Most PHP code does this with substr(), substr_replace() and str_repeat(), which all work on bytes. With accented letters, emoji or non-Latin scripts, that can cut a character in half and leave you with a broken string. This RFC from Sepehr Mahmoudi adds grapheme_mask() to the intl extension, which masks by grapheme, the unit a reader sees as one character.

How it works

grapheme_mask(
    string $string,
    string $mask_char,
    int $offset = 0,
    ?int $length = null
): string|false

You pass the string, the mask character, where to start and how many characters to hide:

grapheme_mask('09123456789', '*', 0, 4);
// "****3456789"

grapheme_mask('09123456789', '*', -4);
// "0912345****"

grapheme_mask('1234567890', 'X', -6, -2);
// "1234XXXX90"

The offset and length rules follow substr():

  • A negative $offset counts from the end of the string.
  • A null $length masks through to the end.
  • A negative $length stops that many characters before the end.
  • If $mask_char contains more than one grapheme, only the first one is used.

The family emoji πŸ‘¨β€πŸ‘©β€πŸ‘§β€πŸ‘¦ is many bytes long but a single grapheme, so it counts as one character:

grapheme_mask('πŸ‘¨β€πŸ‘©β€πŸ‘§β€πŸ‘¦apple🍎', '*', 1, 5);
// "πŸ‘¨β€πŸ‘©β€πŸ‘§β€πŸ‘¦*****🍎"

The same applies to letters combined with vowel marks, as in Persian and Arabic text. Like grapheme_substr() and grapheme_strlen(), it returns false when the input isn't valid UTF-8.

The author chose to tackle the grapheme version before a byte-based str_mask(), since multibyte text is where masking breaks. A str_mask() RFC is planned next, and an mb_mask() for mbstring may follow.

What it means for existing code

Nothing breaks. It's a new function in intl.

Where it stands

It's under discussion and targets PHP 8.6. The page planned a vote from July 10 to July 24, 2026, but no voting poll was ever added.