【PHP8.4】絵文字👨‍👩‍👦‍👦を正しく分割できるようになる

`👨‍👩‍👦‍👦`は25バイトありますが、[これで1文字](https://qiita.com/QUANON/items/2539feed89cee2290664)です。

さて、それでは文字列『絵文字👨‍👩‍👦‍👦を分割』を1文字ずつに分けてみましょう。
現在のPHP標準関数では、これを行うことができません。

[mb_str_split](https://www.php.net/manual/ja/function.mb-str-split.php)すると`['絵', '文', '字', '👨', '‍', '👩', '‍', '👦', '‍', '👦', 'を', '分', '割']`になってしまいます。

間の空白なに？

これは[書記素クラスタ](https://qiita.com/_sobataro/items/47989ee4b573e0c2adfc#%E6%9B%B8%E8%A8%98%E7%B4%A0%E3%82%AF%E3%83%A9%E3%82%B9%E3%82%BF-grapheme-cluster)というもので、正直さっぱり理解できないんだけど、なんか[複数の文字をくっつけて1文字にするという特殊な文字](https://unicode.org/reports/tr29/)みたい。
そしてmb_str_splitはそんな変な文字には対応していないので、律儀に文字ごとに分割してしまうというわけです。

ちなみに[str_split](https://www.php.net/manual/ja/function.str-split.php)はマルチバイトすら対応していないので、`👨‍👩‍👦‍👦`1文字が25文字に分割されます。

ということで**正しく**1文字ごとに分けられる関数`grapheme_str_split`を追加しようというRFCが提出されました。

以下は該当のRFC、[Grapheme cluster for str_split function: grapheme_str_split](https://wiki.php.net/rfc/grapheme_str_split)の日本語訳です。


# PHP RFC: Grapheme cluster for str_split function: grapheme_str_split

## Introduction

PHPには、書記素クラスタ単位のstr_split関数が無いことに気が付きました。
従って、[ICU](https://unicode-org.github.io/icu/userguide/icu4c/)に従った文字列分割関数、`grapheme_str_split`が必要でしょう。
Intl拡張機能がこの関数をサポートすることにより、絵文字や異体字セレクタを正しく処理できるようになります。

`grapheme_str_split`関数は書記素クラスタを正しく処理します。

```php
$ sapi/cli/php -r 'var_dump(grapheme_str_split("🙇‍♂️"));'
array(1) {
  [0]=>
  string(13) "🙇‍♂️"
}
```

`mb_str_split`はUnicodeコードポイント単位での`str_split`であり、こちらのほうが有用な場合ももちろんあります。

```php
$ sapi/cli/php -r 'var_dump(mb_str_split("🙇‍♂️"));'
array(4) {
  [0]=>
  string(4) "🙇"
  [1]=>
  string(3) "‍" // U+200D Zero Width Joinner
  [2]=>
  string(3) "♂"
  [3]=>
  string(3) "️" // U+FE0F VARIATION SELECTOR
}
```

これまで、書記素クラスタを正しく処理するにはPCRE関数に頼る必要がありました。

```php
$ sapi/cli/php -r  'preg_match_all("/(\X)/u", "🙇‍♂️", $matches, PREG_OFFSET_CAPTURE); var_dump($matches[1]);'
array(1) {
  [0]=>
  array(2) {
    [0]=>
    string(13) "🙇‍♂️"
    [1]=>
    int(0)
  }
}
```

他言語の例として、Rubyは[String#grapheme_clusters](https://ruby-doc.org/3.2.2/String.html#method-i-grapheme_clusters)をサポートしています。

```ruby
s = "\u0061\u0308-pqr-\u0062\u0308-xyz-\u0063\u0308" # => "ä-pqr-b̈-xyz-c̈"
s.grapheme_clusters
# => ["ä", "-", "p", "q", "r", "-", "b̈", "-", "x", "y", "z", "-", "c̈"]
```

## Proposal

関数`grapheme_str_split`を追加します。

```php
function grapheme_str_split(string $string, int $length = 1): array|false {}
```

`$string`はUTF-8のみをサポートします。
`$length`は分割する文字列長です。

## Backward Incompatible Changes

ユーザランドに同名の関数があると動かなくなります。

## Proposed PHP Version(s)

PHP8.4

## RFC Impact

### To SAPIs

全てのPHP環境に追加されます。

### To Existing Extensions

Intl拡張に`grapheme_str_split`が追加されます。

## Future Scope

今のところなし。

## Proposed Voting Choices

投票期間は2024/03/26～2024/04/10、投票の2/3の賛成で受理されます。
この期間[メーリングリスト](https://externals.io/message/122744)だけ記載されていて[RFC](https://wiki.php.net/rfc/grapheme_str_split)のほうには書かれてないんですよね。

本RFCは賛成19反対0の全員賛成で可決されました。


## Implementation

[https://github.com/php/php-src/pull/13580](https://github.com/php/php-src/pull/13580)


# 感想

ということで今後は関数`grapheme_str_split`を用いることで、`['絵', '文', '字', '👨‍👩‍👦‍👦', 'を', '分', '割']`と正しく分けられるようになります。

文字コードの話はいくら読んでもあまりに魔境すぎて全くついていけそうな気がしません。
[プルリク](https://github.com/php/php-src/pull/13580)を見ると[意外とあっさりしたかんじの実装](https://github.com/php/php-src/pull/13580/files)に見えるのですが、これは[PHPに元々組み込まれているGrapheme](https://www.php.net/manual/ja/ref.intl.grapheme.php)を使っているからのようです。

で、これを一から実装すると[こんなかんじ](https://github.com/ufcpp/GraphemeSplitter/blob/master/GraphemeSplitter/Character.GetGraphemeBreakPropertyV10.cs)らしい。
正気か？