A new str_mask() RFC proposes a native PHP function for hiding part of a string, like all but the last four digits of a card number. Sepehr Mahmoudi opened the discussion on internals on September 18, targeting PHP 8.7, and the thread has run to about 30 emails since.

Masking shows up in logs, audit trails, and account pages in almost every app, and today you write it yourself. Most of the thread is about whether that should change.

What It Does

Here is the proposed signature:

function str_mask(
    string $string,
    string $mask_char = '*',
    int $offset = 0,
    ?int $length = null
): string {}

And a few examples from the RFC:

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

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

$offset and $length work the way they do in substr(), including negative values that count from the end. The function is byte-oriented, so $mask_char has to be exactly one byte, and an offset outside the string throws a ValueError.

That last part changed during the discussion. The first version returned the original string when the offset was out of range, and Pratik Bhujel pointed out that a masking function which fails by showing you the unmasked value is the wrong way around. The RFC now fails closed. An early #[\SensitiveParameter] attribute on $string was also dropped after Morgan argued that sensitivity belongs to the data, not to a string function.

The Pushback

The main objection is that PHP can already do this. Osama Aldemeery replied that the proposal "looks identical to the existing substr_replace," and the RFC's own examples come out the same:

echo substr_replace("1234567812345678", str_repeat("*", 12), 0, -4);
// ************5678

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

The RFC's answer is performance (one allocation in C instead of a temporary string from str_repeat()) and stricter boundary handling. Several replies weren't convinced. Ilia Alshanetsky asked why it's needed in PHP itself, and suggested masking fits better in a library with modules for different kinds of data.

Pratik also dug into prior art. Laravel's Str::mask() and CakePHP 5.4's Text::mask() do the same core operation, but both are multibyte-aware and return the original string for ranges that select nothing. So core wouldn't be standardizing what frameworks already ship. It would be changing the contract.

What Happens Next

The RFC is at version 0.2 with a Draft status and no implementation yet, so nothing is close to a vote. Unicode-aware masking is listed as future scope for ext/intl through a separate grapheme_mask().

My personal feeling is this will probably not pass the vote but I do think it's a useful feature since both Laravel and CakePHP have their methods for it.

Read the full str_mask() RFC and follow the thread on externals.io.