PHP has a padding function for bytes, str_pad(), and one for code points, mb_str_pad() (PHP 8.3). It has no padding function for grapheme clusters, the unit the grapheme_* functions of ext/intl use for “a character as a user sees it”. This RFC proposes grapheme_str_pad(), as already suggested in the Future Scope of the mb_str_pad RFC.
The same visible text should get the same padding:
<?php $nfc = "Caf\u{00E9}"; // "Café", é as one code point (U+00E9) $nfd = "Cafe\u{0301}"; // "Café", e followed by U+0301 COMBINING ACUTE ACCENT var_dump(mb_str_pad($nfc, 6, '.')); // string(7) "Café.." var_dump(mb_str_pad($nfd, 6, '.')); // string(7) "Café." one dot less var_dump(grapheme_str_pad($nfc, 6, '.')); // string(7) "Café.." var_dump(grapheme_str_pad($nfd, 6, '.')); // string(8) "Café.." ?>
Add the following function to ext/intl:
<?php function grapheme_str_pad(string $string, int $length, string $pad_string = " ", int $pad_type = STR_PAD_RIGHT): string|false {} ?>
It pads $string on the left, the right or both sides with $pad_string until the result is $length grapheme clusters long. $length and the length of the input are counted in grapheme clusters, as grapheme_strlen() counts them.
The function mirrors mb_str_pad() and str_pad():
$length is negative, or less than or equal to the length of $string, $string is returned unchanged.$pad_type is one of the existing constants STR_PAD_LEFT, STR_PAD_RIGHT (default) or STR_PAD_BOTH. With STR_PAD_BOTH the extra grapheme cluster, if any, goes to the right, like in str_pad().$pad_string is repeated as often as needed. If the last copy does not fit entirely, it is cut on a grapheme cluster boundary. The padding string is never split inside a grapheme cluster.str_pad(). The two-byte sequence “\r\n” is a single grapheme cluster, as in all grapheme_* functions.
mb_str_pad() counts code points. Many characters a user perceives as one are made of several code points: letters with combining marks (text in Unicode normalization form NFD, which some platforms and input methods produce), Japanese kana with a combining dakuten (が = か + U+3099), most Indic scripts, emoji with skin tone modifiers and emoji ZWJ sequences. For such text mb_str_pad() adds too little padding, and it can split the padding string inside a grapheme cluster, which changes the padding text itself:
<?php // Padding string "üö" in NFD: u + U+0308, o + U+0308 (4 code points, 2 grapheme clusters) var_dump(mb_str_pad("a", 4, "u\u{0308}o\u{0308}")); // string(5) "aüo" ö lost its diaeresis var_dump(grapheme_str_pad("a", 4, "u\u{0308}o\u{0308}")); // string(10) "aüöü" $ga = "\u{304B}\u{3099}\u{304E}"; // "がぎ": が is か + U+3099, ぎ is precomposed var_dump(mb_str_pad($ga, 4, '*')); // string(10) "がぎ*" var_dump(grapheme_str_pad($ga, 4, '*')); // string(11) "がぎ**" var_dump(mb_str_pad("👍🏽", 3, '-', STR_PAD_LEFT)); // string(9) "-👍🏽" var_dump(grapheme_str_pad("👍🏽", 3, '-', STR_PAD_LEFT)); // string(10) "--👍🏽" ?>
The grapheme_* functions exist precisely for this unit. Adding the padding function completes the set of byte, code point and grapheme cluster variants, as grapheme_str_split() (PHP 8.4) and grapheme_strrev() (PHP 8.6) did for str_split() and strrev().
A correct userland version needs grapheme_strlen() for the input, grapheme_strlen() and grapheme_substr() for the padding string, and the same left/right arithmetic as str_pad(). Symfony's String component does exactly this in UnicodeString::padStart(), padEnd() and padBoth(), whose length is grapheme_strlen(). Everybody else has to reimplement it, or use mb_str_pad() and get the results shown above.
Same as mb_str_pad():
$pad_string throws a ValueError, unless no padding is needed.$pad_type other than STR_PAD_LEFT, STR_PAD_RIGHT or STR_PAD_BOTH throws a ValueError.
Like grapheme_strlen(), invalid UTF-8 in $string or $pad_string makes the function return false and sets the intl error (intl_get_error_code() / intl_get_error_message()).
<?php grapheme_str_pad("abc", 5, ""); // ValueError: grapheme_str_pad(): Argument #3 ($pad_string) must not be empty grapheme_str_pad("abc", 5, "-", 42); // ValueError: grapheme_str_pad(): Argument #4 ($pad_type) must be STR_PAD_LEFT, STR_PAD_RIGHT, or STR_PAD_BOTH var_dump(grapheme_str_pad("abc\xFF", 6, ".")); // bool(false) echo intl_get_error_message(); // grapheme_str_pad(): Invalid UTF-8 string: U_INVALID_CHAR_FOUND ?>
A userland function named grapheme_str_pad() in the global namespace would conflict. No definitions were found on 2026-11-03 with GitHub code search (“function grapheme_str_pad(”, PHP) or Sourcegraph (grapheme_str_pad, PHP). No other changes.
Next PHP 8.x (PHP 8.7).
None beyond knowing the new function.
Adds grapheme_str_pad() to ext/intl. No change to existing functions.
None.
None.
mb_strwidth()) is a different problem and is not addressed by this RFC. grapheme_str_pad() counts grapheme clusters, consistently with the other grapheme_* functions.Primary Vote requiring a 2/3 majority to accept the RFC:
Implementation with tests: https://github.com/php/php-src/pull/25179
After the RFC is implemented, this section will contain:
None yet.