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
$offsetcounts from the end of the string. - A
null$lengthmasks through to the end. - A negative
$lengthstops that many characters before the end. - If
$mask_charcontains 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.