【PHP8.3】日本語でもstr_padできるようになるよ

```php
$str = 'あ';
$str = str_pad($str, 5, 'い');
var_dump($str);
```

![01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/26088/924c6cf4-32d2-766d-da79-ce68b30b1202.png)

その後ろの変なのはなんだ？

ということで[マルチバイト文字列関数](https://www.php.net/manual/ja/ref.mbstring.php)にめでたく`mb_str_pad`が追加される運びになりました。

以下は該当のRFC、[mb_str_pad](https://wiki.php.net/rfc/mb_str_pad)の日本語訳です。

# PHP RFC: mb_str_pad

## Introduction

PHPでは、様々な文字列関数についてふたつのバリアントが存在します。
すなわち1バイト文字とマルチバイト文字です。
しかし、何故かマルチバイト文字列関数には[str_pad](https://www.php.net/manual/ja/function.str-pad.php)に相当する[mb_str_pad](https://www.php.net/manual/ja/function.mb-str-pad.php)が存在しません。
str_padはマルチバイト文字をサポートしていないため、UTF-8などのマルチバイト文字を使う場合に問題が発生します。
本RFCでは関数`mb_str_pad()`の追加を提案します。

## Proposal

本Proposalでは、新しいマルチバイト文字列関数`mb_str_pad`を導入します。
入力文字とパディング文字のいずれもがマルチバイト文字列である可能性があります。
構文は`str_pad`と同じですが、追加で文字コード引数を受け取ります。
文字コード引数は、他のマルチバイト文字関数と同じであり、入力文字とパディング文字両方に適用されます。
引数がない場合はデフォルトの文字コードが使用されます。
引数`$pad_type`は`str_pad`と同じく`STR_PAD_LEFT`・`STR_PAD_RIGHT`・`STR_PAD_BOTH`のいずれかです。

```php
function mb_str_pad(
    string $string,
    int $length,
    string $pad_string = " ",
    int $pad_type = STR_PAD_RIGHT,
    ?string $encoding = null
): string {}
```

### Error conditions

`mb_str_pad`のエラーは、基本的に`str_pad`と同じ条件で発生します。

・`$pad`は空白であってはならない。
・`$pad_type`は`STR_PAD_LEFT`・`STR_PAD_RIGHT`・`STR_PAD_BOTH`のいずれかでなければならない。

追加のエラーが一つ存在します。

・`$encoding`は有効であり、サポートされている文字コードでなければならない。


### Examples and Comparison Against str_pad()


マルチバイト文字列への`str_pad()`と`mb_str_pad()`の動作のちがいを示します。
`str_pad()`では一部の言語や記号において使われているマルチバイト文字で問題が発生します。

最初の例は`Français`という単語に対するものです。
これは一見8バイトですが、`ç`は2バイトであるため実際は9バイトの長さになっています。
従って`mb_str_pad()`では正しく動作しますが、`str_pad()`では想定外の挙動になります。

```php
// 全長が10バイトになる
var_dump(str_pad('Français', 10, '_', STR_PAD_RIGHT));   // string(10) "Français_"
var_dump(str_pad('Français', 10, '_', STR_PAD_LEFT));    // string(10) "_Français"
var_dump(str_pad('Français', 10, '_', STR_PAD_BOTH));    // string(10) "Français_"
 
// 全長が10文字になる
var_dump(mb_str_pad('Français', 10, '_', STR_PAD_RIGHT));// string(11) "Français__"
var_dump(mb_str_pad('Français', 10, '_', STR_PAD_LEFT)); // string(11) "__Français"
var_dump(mb_str_pad('Français', 10, '_', STR_PAD_BOTH)); // string(11) "_Français_"
```

この問題は、アルファベット以外、たとえばギリシャ語などではさらに顕著になります。

```php
var_dump(str_pad('Δεν μιλάω ελληνικά.', 21, '_', STR_PAD_RIGHT));    // string(35) "Δεν μιλάω ελληνικά."
var_dump(str_pad('Δεν μιλάω ελληνικά.', 21, '_', STR_PAD_LEFT));     // string(35) "Δεν μιλάω ελληνικά."
var_dump(str_pad('Δεν μιλάω ελληνικά.', 21, '_', STR_PAD_BOTH));     // string(35) "Δεν μιλάω ελληνικά."

var_dump(mb_str_pad('Δεν μιλάω ελληνικά.', 21, '_', STR_PAD_RIGHT)); // string(37) "Δεν μιλάω ελληνικά.__"
var_dump(mb_str_pad('Δεν μιλάω ελληνικά.', 21, '_', STR_PAD_LEFT));  // string(37) "__Δεν μιλάω ελληνικά."
var_dump(mb_str_pad('Δεν μιλάω ελληνικά.', 21, '_', STR_PAD_BOTH));  // string(37) "_Δεν μιλάω ελληνικά._"
```

絵文字や記号にも必要です。
これは[オリジナルのIssue](https://github.com/php/php-src/issues/10203)の例です。

```php
var_dump(str_pad('▶▶', 6, '❤❓❇', STR_PAD_RIGHT));    // string(6) "▶▶"
var_dump(str_pad('▶▶', 6, '❤❓❇', STR_PAD_LEFT));     // string(6) "▶▶"
var_dump(str_pad('▶▶', 6, '❤❓❇', STR_PAD_BOTH));     // string(6) "▶▶"
 
var_dump(mb_str_pad('▶▶', 6, '❤❓❇', STR_PAD_RIGHT)); // string(18) "▶▶❤❓❇❤"
var_dump(mb_str_pad('▶▶', 6, '❤❓❇', STR_PAD_LEFT));  // string(18) "❤❓❇❤▶▶"
var_dump(mb_str_pad('▶▶', 6, '❤❓❇', STR_PAD_BOTH));  // string(18) "❤❓▶▶❤❓"
```

### Backward Incompatible Changes

`mb_str_pad`自体は新しい関数であり、既存関数に変更はないので、後方互換性のない変更はありません。
ユーザランドで`mb_str_pad`を実装していた場合は致命的なエラー"Cannot redeclare mb_str_pad()"が発生します。

GitHubの[コード検索](https://github.com/search?q=mb_str_pad+lang%3Aphp&type=code)で軽く調べたところ、326件がヒットしました。

このうち12個が`mb_str_pad`の定義前に存在チェックを行っており、42個が存在チェックを行っていませんでした。
従って、そのままアップグレードすると42個の致命的エラーが発生します。
対応方法は、単に関数定義を削除するだけです。

なお、これらの関数実装について自動テストしてみたところ、36個は正しく実装されている可能性が高く、65個は正しく動きませんでした。
このように、正しく実装することは少々むつかしい機能であるということがわかります。

## Proposed PHP Version(s)

PHP8.3

## RFC Impact

### To Existing Extensions

mbstringエクステンションに新関数`mb_str_pad`が追加されます。
既存関数に変更はありません。

## Unaffected PHP Functionality

mbstring以外の全てのPHP機能は、影響を受けません。

## Future Scope

将来の展望であり、本PFCでは実装されません。

コードポイントではなく書記素クラスタ単位で数える`grapheme_str_pad()`関数を追加したい。

## Proposed Voting Choices

投票期間は2023/06/05から2023/06/19であり、全投票の2/3の賛成で受理されます。

2023/06/12時点では賛成9反対1の賛成多数であり、可決される可能性が高いでしょう。

## Patches and Tests

[実装ツリー](https://github.com/nielsdos/php-src/tree/mb_str_pad)

## References

[オリジナルのIssue](https://github.com/php/php-src/issues/10203)


# 感想

これまでも一応[mb_substr](https://www.php.net/manual/ja/function.mb-substr.php)や[mb_strlen](https://www.php.net/manual/ja/function.mb-strlen)などを使って実装することはできていたのですが、PHP本体に実装されることによって今後の運用が楽になります。
ここまできたらもう文字列関数全てにマルチバイト文字列関数を対応させてほしいところですね。

ちなみにユーザコントリビュートに載っていた[最も評価の高いユーザランド実装](https://www.php.net/manual/ja/function.str-pad.php#116244)はこちらです。

```php
function mb_str_pad($str, $pad_len, $pad_str = ' ', $dir = STR_PAD_RIGHT, $encoding = NULL)
{
    $encoding = $encoding === NULL ? mb_internal_encoding() : $encoding;
    $padBefore = $dir === STR_PAD_BOTH || $dir === STR_PAD_LEFT;
    $padAfter = $dir === STR_PAD_BOTH || $dir === STR_PAD_RIGHT;
    $pad_len -= mb_strlen($str, $encoding);
    $targetLen = $padBefore && $padAfter ? $pad_len / 2 : $pad_len;
    $strToRepeatLen = mb_strlen($pad_str, $encoding);
    $repeatTimes = ceil($targetLen / $strToRepeatLen);
    $repeatedString = str_repeat($pad_str, max(0, $repeatTimes)); // safe if used with valid utf-8 strings
    $before = $padBefore ? mb_substr($repeatedString, 0, floor($targetLen), $encoding) : '';
    $after = $padAfter ? mb_substr($repeatedString, 0, ceil($targetLen), $encoding) : '';
    return $before . $str . $after;
}
```

PHP本体で実装されるのは大変ありがたいですね。


# その他

マルチバイト文字列関数、合字には無力です。

```php
$str = '👨🏻‍🦱';

echo mb_str_pad($str, 6, '♡'); // '👨🏻‍🦱'
echo mb_strlen($str); // 6
```

`👨🏻‍🦱♡♡♡♡♡`ってなってほしかったのですが、何故か1文字も入りません。
`👨🏻‍🦱` ← こいつ一見1文字なのですが、実際は6文字なんですよ。
意味がわかりませんね。

[ZWJ](https://ja.wikipedia.org/wiki/%E3%82%BC%E3%83%AD%E5%B9%85%E6%8E%A5%E5%90%88%E5%AD%90)はマジで魔界。
どうしてこんな悪夢みたいな作りになっているんでしょうか。

一般人としてはそんな内部事情など知ったこっちゃありませんから、早いところ見た目の文字数で判定できる関数`grapheme_XXX`の登場が待たれるところですね。