mbstring の関数を PCRE と intl の関数に置き換える


概要
---

UTF-8 だけ扱うプロジェクトにおいて mbstring の関数を PCRE や intl の関数で代用する方法についてまとめました。格安 VPS の普及により、intl だけ導入して mbstring や iconv を導入しなかったり、mbstring の Polyfill が配布されるようになり、既存のライブラリを動かすこと以外に mbstring の関数を使わない選択肢を選ぶことができるようになりました。Polyfill の実装には PCRE が使われています。PHP 5.4 以降で PCRE はコアライブラリです。

不正なバイト列への対応
-------------------

mbstring が暗黙のうちに不正なバイト列を扱うのに対して、PCRE と intl の grapheme 関数は不正なバイト列を扱うことができませんので、代替文字に置き換える必要があります。くわしくはこちらの[記事](http://qiita.com/masakielastic/items/cb1dc16f203c2b7a607e)をご参照ください。


不正なバイト列を代替文字に置き換える
------------------------------

mbstring の `mb_convert_encoding` や `mb_scrub` (PHP 7.2 開発版) は `UConverter::transcode` に置き換えることができます。

```php
mb_substitute_character(0xfffd);

$str = "あい\x80うえお";

var_dump(
    "あい\u{fffd}うえお" === mb_convert_encoding($str, 'UTF-8'),
    // "あい\u{fffd}うえお" === mb_scrub($str),
    "あい\u{fffd}うえお" === UConverter::transcode($str, 'UTF-8', 'UTF-8')
);
```


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

`mb_chr`、`mb_ord` (PHP 7.2 開発版) の代わりに `IntlChar::chr` と `IntlChar::ord` を使うことができます。

```php
var_dump(
    "あ" === IntlChar::chr(0x3042),
    0x3042 === IntlChar::ord("あ")
);
```

文字数を求める
------------

`mb_strlen` の代わりに `preg_match_all` を使います。

```php
$str = "あいうえお";
var_dump(5 === preg_match_all('/./su', $str));
```

`IntlBreakIterator::createCodePointInstance` を使うこともできます。

```php
$str = "あいうえお";

var_dump(
  5 === code_point_count($str)
);

function code_point_count($str)
{
    $it = IntlBreakIterator::createCodePointInstance();
    $it->setText($str);

    $len = 0;

    while (IntlBreakIterator::DONE !== $it->next()) {
        ++$len;
    }

    return $len;
}
```

`grapheme_strlen` は書記素を単位とするので、データベースに保存できる文字数の上限をチェックする目的には使うことができません。書記素を構成するコードポイントの数の上限は存在しないからです。



部分文字列を求める
---------------

`mb_substr` の代わりに `grapheme_substr` を使うことができます。


文字列を切り捨てる
---------------

バイト数単位で文字列を切り捨てる場合、`mb_strcut` の代わりに `grapheme_extract` を使うことができます。

文字幅を求める
-----------

`mb_strwidth` の代わりに `IntlChar` のメソッドを使います。くわしくは[こちら](http://qiita.com/masakielastic/items/53c4f08dcb7e5c97ee24)の記事をご参照ください。

文字列の検索
----------

UTF-8 の文字を構成する先行バイトと後続バイトの範囲は重なっていないので、`mb_strpos` は標準関数の `strpos` に置き換えることができます。

EcmaScript2015 で導入された `startsWith` と `endsWith` に相当する関数を定義する例は stackoverflow の[回答](http://stackoverflow.com/q/834303/531320)をご参照ください。PHP の標準関数に導入するかどうかの [RFC](https://wiki.php.net/rfc/add_str_begin_and_end_functions) もあります。

大文字小文字の変換
---------------

`mb_strtolower`、`mb_strtoupper`、`mb_convert_case($str, MB_CASE_TITLE)` はそれぞれ `IntlChar::tolower`、`IntlChar::toupper`、`IntlChar::totitle`　に置き換えます。Unicode の仕様に関して Ruby 2.4 での対応を解説した[プレゼン資料](http://www.sw.it.aoyama.ac.jp/2016/pub/RubyKaigi/)がわかりやすいです。

標準関数の `strtoupper/strtolower/ucfirst/lcfirst` はロケールによって想定しない文字を変換してしまう可能性があります。くわしくは[こちら](http://qiita.com/masakielastic/items/084805a4429ce4bfb932)の記事をご参照ください。

全角半角の変換
------------

`mb_convert_kana` は `transliterator_transliterate` に置き換えます。

```php
$str = "アイウエオ";
var_dump(
  "ｱｲｳｴｵ" === transliterator_transliterate("Fullwidth-Halfwidth", $str)
);
```

変換を適用する文字を限定するには `preg_replace_callback` を組み合わせます。ひらがなとカタカナはそれぞれ Unicode プロパティで定義されます (`\p{Hiragana}`、`\p{Katakana}`)。

UTF-8 のメールを送信する
---------------------

`mb_send_mail` の代わりに標準関数の `mail` を使います。タイトルと本文に`base64_encode` 関数を適用する必要があります。公式マニュアルの[コメント](http://php.net/manual/ja/function.mail.php#108669)をご参照ください。SMTP 経由で Gmail を送信したいのであれば、curl を使うことができます。


mbstring に存在しない機能
-----------------------

### UTF-8 の区別のしずらい文字が混在していないかチェックする

偽キリル文字のような人間の肉眼では区別できない文字を悪用して、ユーザー名、ドメイン名、メールアドレスのなりすましをする方法があります。対策として Unicode スクリプトプロパティの組み合わせをチェックする方法があり、Gmail が採用しています。intl であれば Spoofchecker を使います。くわしくはこちらの[回答](http://ja.stackoverflow.com/a/2371/568)をご参照ください。

### Unicode 照合アルゴリズムによる比較

Google Chrome のページ内検索、MySQL のようにひらがなとカタカナ、旧字体と新字体を区別なく検索したい場合、Collator を使うことができます。どのような文字を区別したりしなかったりするのかは[こちらの記事](http://qiita.com/masakielastic/items/5559c50632a82da82e26)をご参照ください。