====== PHP RFC: grapheme_mask ====== * Version: 1.0 * Date: 2026-06-30 * Author: Sepehr Mahmoudi * Status: Under Discussion * First Published at: http://wiki.php.net/rfc/grapheme_mask * Discussion: https://news-web.php.net/php.internals/130522 * pr link:https://github.com/php/php-src/pull/22605 ===== Introduction ===== In modern web applications, handling Personally Identifiable Information (PII) securely is a critical requirement. Developers frequently need to obfuscate or mask sensitive data such as phone numbers, email addresses, national IDs, or partial credit card numbers before logging them or displaying them to users (e.g., to comply with privacy regulations). Currently, developers rely on userland implementations combining ''str_repeat'', ''substr'', or ''substr_replace''. However, these native string functions operate exclusively on bytes. When dealing with internationalization (i18n), multibyte characters, combining marks, or emojis, byte-level operations often result in broken strings, corrupted characters, or incorrect masking lengths. The ''grapheme_mask'' function introduces a native, highly performant, and fully Unicode-aware way to mask strings without corrupting complex characters. By utilizing the ICU library, it ensures that grapheme cluster boundaries are always respected. ===== Why grapheme_mask() first instead of str_mask()? ===== A common question might be: *Why not introduce a standard ''str_mask()'' function first?* While a byte-level ''str_mask()'' is highly valuable for ASCII strings and general use cases, handling masking safely in modern UTF-8 environments is a more pressing issue. Using a byte-level function on a string containing multibyte characters where the offset or length falls in the middle of a byte sequence results in invalid UTF-8 characters. Therefore, this RFC prioritizes the grapheme-aware masking function in the ''intl'' extension to solve the more complex and error-prone problem of Unicode masking. **Note:** I am planning to propose a standard ''str_mask()'' for byte-level operations in a subsequent, separate RFC. ===== Proposal ===== This RFC proposes adding a new function to the ''intl'' extension: ''grapheme_mask()''. ==== Function Signature ==== function grapheme_mask( string $string, string $mask_char, int $offset = 0, ?int $length = null ): string|false * **$string**: The original input string. * **$mask_char**: The character (or string) to use for masking. If it contains multiple graphemes, only the first grapheme cluster is used to maintain structural integrity. * **$offset**: The starting grapheme position for masking. * If positive, masking starts at that grapheme offset from the beginning. * If negative, masking starts at that grapheme offset from the end of the string. * **$length**: The number of graphemes to mask. * If omitted or ''null'', it masks from the ''$offset'' to the end of the string. * If negative, masking stops that many graphemes from the end of the string. ===== Return Value ===== Returns the masked string on success, or **false** if the input string is not valid UTF-8 or if the grapheme operation fails. This behavior is consistent with other grapheme functions in the intl extension, such as ''grapheme_substr()'' and ''grapheme_strlen()''. ===== Examples ===== Here are 6 examples demonstrating how ''grapheme_mask()'' works, covering basic usage, negative offsets, and complex Unicode characters: **Example 1: Masking from the Beginning** // Masking the first 4 characters $result = grapheme_mask('09123456789', '*', 0, 4); var_dump($result); // string(11) "****3456789" **Example 2: Masking with Positive Offset and Length** // Masking 3 characters starting from the 5th character $result = grapheme_mask('HelloWorld', '*', 5, 3); var_dump($result); // string(10) "Hello***ld" **Example 3: Masking with Negative Offset** // Masking the last 4 characters of a string // -4 means start 4 graphemes from the end and mask to the end $result = grapheme_mask('09123456789', '*', -4); var_dump($result); // string(11) "0912345****" **Example 4: Masking with Negative Offset and Negative Length** // Start masking 6 graphemes from the end, and stop 2 graphemes from the end $result = grapheme_mask('1234567890', 'X', -6, -2); var_dump($result); // string(10) "1234XXXX90" **Example 5: Handling Complex Emojis (ZWJ Sequences)** // The family emoji (πŸ‘¨β€πŸ‘©β€πŸ‘§β€πŸ‘¦) is multiple bytes but ONE single grapheme cluster. // Masking from offset 1 replaces the apple. $result = grapheme_mask('πŸ‘¨β€πŸ‘©β€πŸ‘§β€πŸ‘¦apple🍎', '*', 1, 5); var_dump($result); // string(14) "πŸ‘¨β€πŸ‘©β€πŸ‘§β€πŸ‘¦*****🍎" **Example 6: Handling Combining Marks (Persian/Arabic Vowels)** // In "Ψ³ΩŽΩ„Ψ§Ω…", "سَ" is one grapheme cluster (Base character + vowel mark). // Masking starting from index 1 will correctly skip the first grapheme. $result = grapheme_mask('Ψ³ΩŽΩ„Ψ§Ω…β€ŒΨ―Ω†ΫŒΨ§', '*', 1, 3); var_dump($result); // string(16) "سَ***Ψ―Ω†ΫŒΨ§" ===== Backward Incompatible Changes ===== None. ===== Proposed PHP Version(s) ===== PHP 8.6 ===== RFC Impact ===== * **SAPIs**: No impact. * **Existing Extensions**: This function will be added to the ''intl'' extension. * **Opcache**: No impact. ===== Open Issues ===== None at the moment. ===== Future Scope ===== As mentioned, a native ''str_mask()'' function operating at the byte level will be proposed in a future RFC to complement ''grapheme_mask()''. Additionally, a multibyte counterpart (''mb_mask'') for the ''mbstring'' extension may be considered for users without the ''intl'' extension. ===== Voting ===== As this is a feature addition, it requires a 2/3 majority in favor to be accepted. Voting will open on 2026-07-10 and close on 2026-07-24.