PHP8.2で非推奨になったエンコーディングたち

この記事は PHP Advent Calendar 2022 13日目の記事です。

つい先日、PHP8.2がリリースされました :tada: 
毎回メジャーリリースでは便利な新機能が追加される一方で、下位互換性のなくなる変更や非推奨になる機能も存在します。
既存システムのPHPをバージョンアップするためには、これらの変更に対応していく必要があります。

ここでは、PHP8.2での変更の1つ「[mbstringの一部エンコーディングの非推奨化](https://www.php.net/manual/ja/migration82.deprecated.php#migration82.deprecated.mbstring)」を取り上げます。

## 変更の概要
mbstringの`mb_convert_encoding()`や`mb_strlen()`などの関数には、`$encoding`という引数があり、引数の文字列の文字コードを指定できます。
ここに指定できるエンコーディングのうち、`qprint`, `base64`, `uuencode`, `HTML-ENTITIES`の4種が非推奨となります。

UTF-8やShift_JISなどが「文字をバイト列にエンコードする」ものであるのに対し、上記4つは「バイト列をバイト列でエンコードする」ものであるというのが理由のようです。

## PHP8.2以降の代替手段
今までに上記のエンコーディングを`$encoding`引数に利用していた場合、PHP8.2からは以下のいずれかの対応になると思います。

- `mb_convert_encoding()`を使っていた場合：他の関数を使って変換を行う
- その他の関数を使っていた場合：他の文字コードに変換した上で扱う

つまり、いずれの場合も`mb_convert_encoding()`相当の変換さえできればいいわけです。
では、各エンコーディングの変換処理をどのように置き換えればいいか、ここから紹介していきます。

### `qprint` (Quoted-printable)
`qprint` ([Quoted-printable](https://ja.wikipedia.org/wiki/Quoted-printable)) は以下の関数で変換できます。

- エンコード：[`quoted_printable_encode()`](https://www.php.net/manual/ja/function.quoted-printable-encode.php)
- デコード：[`quoted_printable_decode()`](https://www.php.net/manual/ja/function.quoted-printable-decode.php)

名前がそのまんまですね :smiley: 
実際に動かしてみても、同じように変換されていることがわかります。

https://3v4l.org/o9OKL

### `base64`
Base64も同様に、PHPの関数で変換できます。

- エンコード：[`base64_encode()`](https://www.php.net/manual/ja/function.base64-encode.php)
- デコード：[`base64_decode()`](https://www.php.net/manual/ja/function.base64-decode.php)

https://3v4l.org/I5ZET

### `HTML-ENTITIES`
この流れで行くと[`htmlentities()`](https://www.php.net/manual/ja/function.htmlentities.php), [`html_entity_decode()`](https://www.php.net/manual/ja/function.html-entity-decode.php)で変換、となりそうですが、これは違う変換をする関数です。

```php
echo htmlentities("<p>Hello世界</p>\n");
echo mb_convert_encoding("<p>Hello世界</p>\n", 'HTML-ENTITIES');
```

```txt:出力結果
&lt;p&gt;Hello世界&lt;/p&gt;
<p>Hello&#19990;&#30028;</p>
```

https://3v4l.org/AKVEe

`HTML-ENTITIES`相当の変換を行うのは、同じmbstringモジュールの[`mb_encode_numericentity()`](https://www.php.net/manual/ja/function.mb-encode-numericentity.php) / [`mb_decode_numericentity()`](https://www.php.net/manual/ja/function.mb-decode-numericentity.php) です。
どちらの関数も第2引数に、文字数値参照に変換する文字の範囲などを表す配列を渡しますが、`[0x80, 0x10ffff, 0, 0x1fffff]`としておくとよさそうです。

https://3v4l.org/UjNMu

参考：
- https://github.com/symfony/symfony/pull/45532/files#diff-940b51ffa31dedac17aa49cd8e04e44aa8b3747782c7f9f27457b0510587c05d
- https://github.com/symfony/symfony/pull/46221/files

### `uuencode`
uuencodeの変換関数は、冒頭で紹介した公式の移行ガイドにも記載されています。

- エンコード：[`convert_uuencode()`](https://www.php.net/manual/ja/function.convert-uuencode.php)
- デコード：[`convert_uudecode()`](https://www.php.net/manual/ja/function.convert-uudecode.php)

これらを使えば解決……と思いきや、uuencodeの場合はそこまで単純ではありません。

#### mbstring と convert_uuencode() / convert_uudecode() の違い
[Wikipedia](https://ja.wikipedia.org/wiki/Uuencode)にあるように、uuencodeは「一行のヘッダー、複数行のエンコード文字列、一行のフッターからなる」フォーマットです。また、エンコード文字列は`` ` ``のみを含む行で終わります。

しかし、PHPが扱うuuencodeのフォーマットは上記の一部のみを満たすもので、さらにmbstringと`convert_uuencode()`とで微妙に異なります。
例えば、`test`という文字列をPHP8.2の`mb_convert_encoding()`と`convert_uuencode()`でエンコードすると、以下の結果になります。

```
# mb_convert_encoding('test', 'uuencode')
begin 0644 filename
$=&5S=```

# convert_uuencode('test')
$=&5S=```
`
```

https://3v4l.org/8k14F#v8.2.0

つまり、以下のようになっています。

||mbstring|`convert_uuencode()`<br>`convert_uudecode()`|
|--|--|--|
|ヘッダー行|○|×|
|エンコード文字列末尾の`` ` ``だけの行|×|○|
|フッター行|×|×|

なぜこうなっているのかは謎ですが、変換を行うにはこの差異を吸収するように自前で実装する必要がありそうです。
ただ、最初や最後の行を付け足したり削除したりするだけなので、さほど難しくはないはずです。

#### `mb_convert_encoding()`によるuuencode変換
上で`mb_convert_encoding('test', 'uuencode')`の結果の例を載せましたが、[3v4l](https://3v4l.org/8k14F) の実行結果をよく見ると、8.0, 8.1では`bool(false)`が出力されています。

実は、uuencodeフォーマットのエンコード処理は8.1まで実装されていなかったようなのです。
それが、8.2で`uuencode`が非推奨になると同時に実装されました。
なぜこうなったのか、詳しい経緯は正直謎ですが、いずれにしてもわざわざ非推奨の変換を今から新しく使うことはないでしょう。

## おわりに
PHP8.2で非推奨になったmbstringのエンコーディングと、その置き換え方法を紹介しました。
PHPバージョンアップの際の参考になれば幸いです。

~~ここまで書いてたらアドベントカレンダー担当日を数分過ぎてしまったのは内緒です。uuencodeがいろいろカオスなのが悪い！()~~