PHP のマルチバイト関数標準化のための取り組み

PHP のマルチバイト関数の標準化について PHP Internals に質問および提案するために、調査したことおよび自分の意見をまとめました。

概要
---

このドキュメントでは標準関数の `strlen`、`substr`、`chr`、`ord` のマルチバイトの文字エンコーディング対応もしくはこれらに相当する新しい標準関数の導入について検討します。

取り組む目的は CMS、フレームワーク開発のための必要最小限の関数を提供することで、mbstring、iconv、intl への依存度を下げるもしくはこれらのモジュールのポリフィルライブラリが UTF-8 以外のより多くのマルチバイトの文字エンコーディングに対応できるようにすることです。

標準のマルチバイト関数が存在しないことの課題
-------------------------------------

mbstring、iconv、intl がインストールされていない環境のために主要な CMS やフレームワークは独自のマルチバイト関数のライブラリを開発したり (Drupal)、フォールバック関数のパッケージを配布しています (Symfony)。

文字エンコーディングのシェア
-----------------------
2010年に公開された Google の[記事](https://googleblog.blogspot.jp/2010/01/unicode-nearing-50-of-web.html)によると約50％のウェブサイトの文字エンコーディングが UTF-8 です。

選択肢
-----

次の選択肢が考えられます。

 1. 標準関数の `strlen`、`substr`、`chr`、`ord` に文字エンコーディングオプションを追加する (デフォルトの文字エンコーディングは `ISO-8859-1` もしくは `UTF-8`)
 2. 新しい名前空間つきの `strlen`、`substr`、`chr`、`ord` (デフォルトの文字エンコーディングは `UTF-8`)
 3. 名前空間なしで `strlen`、`substr`、`chr`、`ord` 以外の関数 (デフォルトの文字エンコーディングは `UTF-8`)

選択肢 1 を選ぶ場合、デフォルトの文字エンコーディングの選択が問題になります。mbstring によるオーバーロード対策から学ぶことができます。

`UTF-8` を選ぶ場合、後方互換性が失われるので、フレームワーク・ライブラリの対応が必要になります。後方互換性を保つために `ISO-8859-1` を選ぶ場合、`UTF-8` を使うには毎回引数に指定する必要があります。

選択肢 2 を選ぶ場合、新しい名前空間の候補を検討する必要があります。

選択肢 3 を選ぶ場合、長い名前、広く使われない名前を選ぶ必要があります。

strlen、substr、chr、ord に相当する関数、クラス
------------------------------------------

mbstring

 * `mb_strlen`
 * `mb_substr`
 * `mb_convert_encoding`
 * `mb_encode_numericentity`
 * `mb_decode_numericentity`

iconv

 * `iconv_strlen`
 * `iconv_substr`
 * `iconv`

intl

 * `grapheme_extract`
 * `IntlBreakIterator`
 * `IntlCodePointBreakIterator`
 * `UConverter`
 * `IntlChar::chr`
 * `IntlChar::ord`

ポリフィルの事例
--------------

 * WordPress: [compat.php](https://core.trac.wordpress.org/browser/trunk/src/wp-includes/compat.php) - `mb_strlen` と `mb_substr`
 * Joomla - [phputf8](https://github.com/joomla-framework/string/tree/master/src/phputf8)
 * Drupal: [Unicode](https://api.drupal.org/api/drupal/core!lib!Drupal!Component!Utility!Unicode.php/class/Unicode/8) クラス
 * MediaWiki: [Fallback](https://github.com/wikimedia/mediawiki/blob/master/includes/Fallback.php)
 * Symfony: [Polyfill](https://github.com/symfony/polyfill) コンポーネント - mbstring、iconv、intl の各種関数の実装

コードポイントと文字の相互変換。

 * WordPress - [`wp_encode_emoji`](https://developer.wordpress.org/reference/functions/wp_encode_emoji/)
 * HTMLPurifier - [`unichr`](https://github.com/ezyang/htmlpurifier/blob/master/library/HTMLPurifier/Encoder.php)

文字からコードポイントを求めて、JavaScript エスケープシーケンスに変換する事例として次のプロジェクトが挙げられます。[Escaping RFC for PHP Core](https://wiki.php.net/rfc/escaper) も参照。JSON エスケープシーケンスを求めるには `json_encode` を使うこともできます。

 * Composer - [JsonFormatter](https://github.com/composer/composer/blob/7a9eb02190d334513e99a479510f87eed18cf958/src/Composer/Json/JsonFormatter.php)
 * Zend Escaper - [jsMatcher](https://github.com/zendframework/zend-escaper/blob/c7d68ca80092fd6008df8b89cb6ff86ace26f95c/src/Escaper.php#L273)
 * Twig - [twig_escape_filter](https://github.com/twigphp/Twig/blob/master/lib/Twig/Extension/Core.php#L996)
 
マルチバイト対応の標準関数の現状
---------------------------

PHP 5.3.0 で PCRE モジュールが標準化され、UTF-8 および拡張書記素クラスターを扱う関数を定義することが手軽になりました。mbstring、iconv の関数とも遜色ない処理速度なので、ポリフィルライブラリの実装にも広く使われています。`preg_match` を使った例は[こちら](http://blog.sarabande.jp/post/66950475863)、`preg_replace_callback` を使った例は[こちら](http://blog.sarabande.jp/post/60437186122)の記事を参照ください。

`utf8_decode` は `strlen` と組み合わせて、文字数を数えるために使われることがあります。

```php
$str = 'あいうえお';
$length = strlen(utf8_decode($str);
```
`htmlspecialchars` および `htmlspecialchars_decode` は複数の文字エンコーディングに対応しており、この2つの関数を組み合わせてさまざまなマルチバイト関数を定義することは可能ですが、実用的な速度ではありません。コードの例は[こちら](http://blog.sarabande.jp/post/59745219704)をご参照ください。

コードポイントから文字への変換には `html_entity_decode` を使うことができます。拡張モジュールを含めたコードの例に関して、次の記事をご参照ください。

 * [コードポイントから UTF-8 の文字を生成する](http://qiita.com/masakielastic/items/68f81e1b7d153ee5cc81)
 * [UTF-8 の文字からコードポイントを求める](http://qiita.com/masakielastic/items/5696cf90738c1438f10d)


C 言語のマルチバイト対応の標準関数
-----------------------------

C 言語の標準関数の中にはマルチバイトに対応しているものがあります (詳しい内容は[こちら](http://qiita.com/masakielastic/items/37914cbf89c8c827d1f3))。しかしながら、ロケールに依存する問題や不正なバイト列を扱えない課題を抱えているために、実装に使うことはできません。ただし、自前の文字列関数を定義する際の参考にはなります。UTF-8 の不正なバイト列の扱いの実装に関しては、[こちら](http://qiita.com/masakielastic/items/432b774569b44f959b1c)の記事をご参照ください。

提案
---

### 名前空間ありの関数

新しい名前空間とともに次の関数を提案します。

```php
int strlen ( string $string[, string $encoding = "UTf-8"] )
string substr ( string $string , int $start [, int $length [, string $encoding = "UTF-8"]] )
string chr ( int $codepoint [, string $encoding = "UTF-8"] )
int ord ( string $string[, string $encoding = "UTF-8"] )
```

デフォルトの文字エンコーディングは `UTF-8` とします。mbstring、iconv との互換性のため、`htmlspecialchars` がサポートする文字エンコーディングを選べるようにすることが望ましいと考えます。

実装例は次のとおりです。

 * `strlen` - [UTF-8 限定の実装例](http://blog.sarabande.jp/post/57158304150)
 * `substr` - [UTF-8 限定の実装例](http://blog.sarabande.jp/post/60073351306)
 * `chr` - [ord と合わせた実装例](http://blog.sarabande.jp/post/112506032343)
 * `ord`
 
### 名前空間なしの標準関数

名前空間が受理されない場合の提案です。関数の名前の参考事例として Java や JavaScript が挙げられます。

Java

 * `Java.lang.String.codePointCount`

JavaScript

 * `String.prototype.charAt`
 * `String.prototype.charCodeAt`
 * `String.fromCharCode`
 * `String.prototype.at` - ES7 への[提案](https://github.com/mathiasbynens/String.prototype.at)
 * `String.prototype.codePointAt`
 * `String.fromCodePoint`

#### str_codepoint_count

```php
int str_codepoint_count ( string $string, [, string $encoding = "UTF-8"] )
```

実装例は名前空間つきの `strlen` と同じです。
 
#### `str_at`

```php
string str_at ( string $string, int $index[, string $encoding = "UTF-8"] )
```

[UTF-8 限定の実装例](http://blog.sarabande.jp/post/133993983673)

#### `str_codepoint_at`

```php
int str_codepoint_at ( string $string, int $index[, string $encoding = "UTF-8"] )
```

[UTF-8 限定の実装例](http://blog.sarabande.jp/post/133992781023)

#### `str_from_codepoint`

```php
string str_from_codepoint (array $codepoints[, string $encoding = "UTF-8"] )
```

[UTF-8 限定の実装例](http://blog.sarabande.jp/post/134007302918)

### 不正なバイト列の扱いに関する方針

不正なバイト列に遭遇したとき、処理を続けるか `null` を返すのか方針を決める必要があります。`htmlspecialchars` や mbstring の場合、処理を続け、PCRE や intl の場合、`null` を返します。

### マルチバイト文字を扱うための PHP API

すでに UTF-8 を扱うためのための PHP API が存在します([ext/standard/html.c](https://github.com/php/php-src/blob/master/ext/standard/html.c))。

  * `php_next_utf8_char`

`php_next_utf8_char` は `get_next_char` のショートカットで `utf8_decode` の実装に使われています。`get_next_char` は`htmlspecialchars` の実装に使われています。

mbstring や iconv がサポートする多くのマルチバイトの文字エンコーディングを選べるように、`get_next_char` および `determine_charset` を static から PHP API に変更することを提案します。

 * `php_get_next_char`
 * `php_determine_charset`

その他
-----

### 1文字ずつコールバックを適用する関数

`str_at`、`str_codepoint_at` と並行して次の関数も検討対象として挙げます。
これらの関数の参考例として Ruby の String クラスの `each_char`、`each_codepoint` が挙げられます。

`str_each_char` の別の名前の候補として `str_replace_callback` を挙げます。PHP の似たような関数の先行事例として `preg_replace_callback` と `mb_ereg_replace_callback` があります。


#### `str_each_char`
[UTF-8 限定の実装例](http://blog.sarabande.jp/post/57645700697)

#### `str_each_codepoint`
[UTF-8 限定の実装例](http://blog.sarabande.jp/post/57674430815)

#### str_each_char

```php
void str_each_char ( string $string[, callable $callback [, string $encoding = "UTF-8" ]] )
```

#### str_each_codepoint

```php
void str_each_codepoint ( string $string[, callable $callback [, string $encoding = "UTF-8" ]] )
```

### 部分文字列を取り出す関数

関数の名前の用例は Rust、Groovy (StringGroovyMethods)、Haskell (List) があります。

Rust
 * `truncate` - std::string::String

Groovy の StringGroovyMethods

 * take
 * drop
 * takeWhile
 * dropWhile

#### `str_take`
[UTF-8 限定の実装例](http://blog.sarabande.jp/post/134010595628)

#### `str_take_while`
[UTF-8 限定の実装例](http://blog.sarabande.jp/post/57764752539)

#### `str_drop`
[UTF-8 限定の実装例](http://blog.sarabande.jp/post/134011585213)

#### `str_drop_while`
[UTF-8 限定の実装例](http://blog.sarabande.jp/post/59945616086)


### 文字列イテレーター

一文字取り出す方法として、イテレーターのクラスの導入も挙げられます。
 
#### `StringIterator`
[UTF-8 限定の実装例](http://blog.sarabande.jp/post/62918275916)

#### `CodePointIterator`
[UTF-8 限定の実装例](http://blog.sarabande.jp/post/63001798634)

### 不正なバイト列を扱うための関数

余裕があれば、不正なバイト列を扱うための関数も提案します。

#### `str_check_encoding`

[UTF-8 限定の実装例](http://blog.sarabande.jp/post/57175031973)

#### `str_scrub`

[UTF-8 限定の実装例](http://blog.sarabande.jp/post/57154160056)

`str_check_encoding` の先行事例は `mb_check_encoding`、`str_scrub` は Ruby の String クラスの `scrub` です。

### filter モジュール

提案される標準関数に対応する filter モジュールのフィルターを追加することも検討できます。

#### `FILTER_VALIDATE_STRING_LENGTH`
 
 最小文字数と最大文字数の検証フィルター。[実装例](http://blog.sarabande.jp/post/67630557935)

#### `FILTER_VALIDATE_ENCODING`

文字エンコーディングが妥当であるかの検証フィルター。[実装例](http://blog.sarabande.jp/post/67551711855)

#### `FILTER_ESCAPE_ILL_FORMED_BYTES`

不正なバイト列を代替文字に置き換える除去フィルター。[実装例](http://blog.sarabande.jp/post/67459697392)

### mbstring への新しい関数

標準関数への提案と並行して、mbstring にも対応する関数も提案します。逆に mbstring で先に導入して試してから、標準関数に導入することを提案するというシナリオも考えられます。
  
#### `mb_chr`

PHP 7.2 で導入される予定です。

[実装例](http://blog.sarabande.jp/post/107516655128)

#### `mb_ord`

PHP 7.2 で導入される予定です。

[実装例](http://blog.sarabande.jp/post/107513491438)

#### `mb_at`
[実装例](http://blog.sarabande.jp/post/107495678203)

#### `mb_codepoint_at`
[実装例](http://blog.sarabande.jp/post/134042613553)

#### `mb_from_codepoint`
[実装例](http://blog.sarabande.jp/post/134113502803)

#### `mb_each_char`
[実装例](http://blog.sarabande.jp/post/107579538868)

#### `mb_each_codepoint`
[実装例](http://blog.sarabande.jp/post/107588770273)

#### `mb_scrub`

PHP 7.2 で導入される予定です。

[実装例](http://blog.sarabande.jp/post/107518030538)

#### `mb_take`
[実装例](http://blog.sarabande.jp/post/134014894788)

#### `mb_drop`
[実装例](http://blog.sarabande.jp/post/134015086183)

mbstring がサポートする文字エンコーディングの中にはコードポイントと文字の相互変換の用途に想定していないものがあるので、それらの文字エンコーディングは変換の対象として禁止にします。