====== PHP RFC: str_mask() ======
* Version: 1.0
* Date: 2026-07-01
* Author: Sepehr Mahmoudi, sepehrphpr@gmail.com
* Status: Draft
* Target Version: PHP 8.7
===== Introduction =====
In modern web development, obfuscating or "masking" sensitive user data (such as credit card numbers, phone numbers, email addresses, and national IDs) is a daily necessity for security and privacy compliance (e.g., GDPR). Currently, PHP developers have to rely on a combination of functions like substr, str_repeat, str_pad, or regular expressions (preg_replace). These user-land implementations reduce code readability and are prone to off-by-one errors if not carefully crafted.
The goal of introducing the str_mask() function is to provide a native, fast, and standard tool in the PHP core to perform string masking with the highest performance and the cleanest syntax possible. This function serves as the byte-level counterpart to the grapheme_mask() function proposed for the intl extension, together providing a complete toolkit for string masking in both ASCII and multi-byte environments.
===== Proposal =====
The new str_mask function will be defined in the global namespace with the following signature:
function str_mask(
string $string,
string $mask_char = '*',
int $offset = 0,
?int $length = null
): string {}
===== Parameters =====
* **$string**: The input string being masked. If empty, an empty string is returned.
* **$mask_char**: The character used to mask the specified portion of the string. The default value is an asterisk (*). If a string longer than one byte is provided, only the first byte is used.
* **$offset**: The starting position of the mask. This parameter is highly flexible:
* If **positive**, masking begins at this zero-based index from the start of the string.
* If **negative**, masking begins at that many characters from the end of the string. For example, an offset of -4 means masking starts at the fourth character from the end. This is extremely useful when the exact length of the string is unknown, but the tail end needs to be modified.
* If the absolute value of offset is greater than the string length, the function returns the original string unchanged.
* **$length**: The length of the portion to be masked:
* If **null** (default), masking continues from the $offset until the end of the string.
* If **positive**, exactly this many characters are masked starting from the $offset.
* If **negative**, masking starts at the $offset and stops exactly this many characters from the end of the string.
* If **0**, no changes are made, and the original string is returned.
===== Return Value =====
Returns the masked string. If the input string is empty, an empty string is returned. If offset is out of bounds, the original string is returned unchanged.
===== Examples =====
==== Example 1: Masking a Credit Card Number ====
Mask all digits of a 16-digit credit card except for the first 4 and the last 4 digits.
$credit_card = '1234567890123456';
$masked_card = str_mask($credit_card, '*', 4, 8);
echo $masked_card; // Output: 1234********3456
==== Example 2: Masking a Phone Number (Negative Offset) ====
Hide the last 4 digits of a phone number using a negative offset. This is particularly useful when the total length of the string may vary.
$phone_number = '+989123456789';
$masked_phone = str_mask($phone_number, 'X', -4);
echo $masked_phone; // Output: +9891234XXXX
==== Example 3: Masking an Email Username (Negative Length) ====
Keep the first letter visible, mask everything up to the @ symbol, and leave the domain intact. The negative length allows us to stop masking before the domain without calculating the exact length.
$email = 'sepehr.developer@example.com';
$at_position = strpos($email, '@');
$distance_from_end = -(strlen($email) - $at_position);
$masked_email = str_mask($email, '*', 1, $distance_from_end);
echo $masked_email; // Output: s***************@example.com
==== Example 4: Handling Out of Bounds Offsets ====
If the offset is beyond the string length, the function returns the original string unchanged.
$api_token = 'AB';
$masked_short = str_mask($api_token, '*', 3);
echo $masked_short; // Output: AB
==== Example 5: Masking with Default Parameters ====
Using the default mask character (*) and masking from the beginning to the end.
$text = 'Hello World';
$masked = str_mask($text);
echo $masked; // Output: ***********
===== Backward Incompatible Changes =====
None. This is a new function added to the core and does not break any existing functionality.
===== Proposed PHP Version(s) =====
PHP 8.7
===== RFC Impact =====
* **SAPIs**: No impact.
* **Existing Extensions**: No impact.
* **Opcache**: No impact.
* **Performance**: The function is implemented in C and is significantly faster than user-land implementations.
===== Future Scope =====
This function serves as the byte-level counterpart to the grapheme_mask() function proposed for the intl extension in a separate RFC. Together, they will provide a complete toolkit for string masking in both ASCII and multi-byte environments.
===== Voting =====
As this is a feature addition, it requires a 2/3 majority in favor to be accepted. Voting will open on 2026-07-20 and close on 2026-08-03.