Apps often hide part of a string before displaying or logging it, like a card number shown as ************5678. PHP has no built-in way to do this, so this RFC from Sepehr Mahmoudi adds a str_mask() function.

Show me

Today you'd combine substr_replace() and str_repeat():

$masked = substr_replace($input, str_repeat('*', $maskLength), $offset, $length);

With the RFC, you'd write:

$masked = str_mask($input, '*', $offset, $length);

Here are the RFC's examples:

echo str_mask("1234567812345678", "*", 0, -4);
// ************5678

echo str_mask("0012345678", "#", 2, 6);
// 00######78

echo str_mask("secret_token_abc", "X", 7);
// secret_XXXXXXXXX

How it works

function str_mask(
    string $string,
    string $mask_char = '*',
    int $offset = 0,
    ?int $length = null
): string {}
  • $mask_char must be exactly one byte. An empty string or a multibyte character, like an emoji, throws a ValueError.
  • $offset is where masking starts, and a negative value counts from the end.
  • $length is how many bytes to mask. null means mask to the end, and a negative value stops that many bytes before the end.

The function "fails closed." If the offset or length is out of range, it throws a ValueError instead of returning the original string, so a bug in your math can't leak the data you meant to hide:

// Throws ValueError: Offset is out of bounds
str_mask("hello", "*", 10);

Like str_pad(), the function works on bytes. Masking by visible character could come later in the intl extension, as something like grapheme_mask().

The RFC points to similar framework helpers, like CakePHP's Text::mask() and Laravel's Str::mask().

What it means for existing code

Nothing breaks unless you've already declared your own global str_mask() function, which would clash.

Where it stands

Sepehr Mahmoudi announced the RFC on the internals list on September 18, 2026, though the wiki page still says Draft. Pratik Bhujel pointed out that the first version returned the string unmasked when the offset was out of range, and used only the first byte of the mask character. Sepehr has since revised the RFC to throw a ValueError in both cases. He also dropped #[SensitiveParameter] from the signature, saying sensitivity belongs to the caller's data, not to the function. Other replies question whether the function belongs in core when userland code can do the same job.

It targets PHP 8.7, and there's no implementation yet. A vote would need a two-thirds majority.