💱

PHP/Laravelで文字コードの変換可否を判定する方法

2023/12/10に公開

こちらの記事は、アルサーガーパートナーズアドベントカレンダーの10日目の参加記事です。
他の記事は下記リンクをご参照ください。

https://qiita.com/advent-calendar/2023/arsaga

はじめに

はじめまして。
アルサーガパートナーズ株式会社でサーバーサイドエンジニアをしております、たっしーと申します。

以前携わった開発でタイトルの内容についての実装することがあり、要件としてはシンプルな内容ですが想像以上に時間を溶かしてしまったことがありました。

先輩方のアドバイスを受けつつ最終的には実現することができましたが、今後、私以外の誰かの助けになればと思い、ここにまとめます。

環境

PHP 8.2.7
Laravel 10.5.1

やりたいこと

入力値の文字コードが「UTF-8」から「Shift-JIS」に変換可能である場合のみ、サーバー側のバリデーションを通過させたい。

※主に旧字体などを弾きたい

最初の実装

phpには文字列が指定したエンコーディングで有効なものかどうかを調べるmb_check_encoding()という関数があるので、それを使って下記のようなカスタムルールを作りました。

class CanConvertUTF8ToShiftJIS implements ValidationRule
{
    public function validate(string $attribute, mixed $value, Closure $fail): void
    {		
        if (!mb_check_encoding($value, "Shift_JIS")) {
            $fail("$attribute contains characters that cannot be registerd.");
        }
    }
}

検証

上記の実装をして終わりと思っていましたが、実際に検証すると結果は期待するものとは違っていました。

入力値:6パターン

0 => '田中太郎',
1 => '田中太',
2 => '田中',
3 => '田',
4 => '髙',  // はしごだか:変換不可
5 => '髙田',  // はしごだか:変換不可

期待する出力

0 => false
1 => false
2 => false
3 => false
4 => true 
5 => true

実際の出力

0 => true  // 期待:false
1 => false
2 => false
3 => false
4 => true 
5 => false  // 期待:true

ローカルでの変なバグかと思い、各種環境のリフレッシュなど試しましたが現象は変わらず・・・

調べてみると、mb_check_encoding()は入力値の内容によっては正常に機能しない場合があるようです。

https://qiita.com/a_utsuki/items/9e33ec6e899994f89218
https://hnw.hatenablog.com/entry/20090215

その後の検証と実装

判定をするmb_check_encoding()が期待する挙動ではないので、実際にエンコーディングする関数である、mb_convert_encoding()の挙動を確認してみました。

入力値:6パターン

0 => '田中太郎',
1 => '田中太',
2 => '田中',
3 => '田',
4 => '髙',  // はしごだか
5 => '髙田',  // はしごだか

出力

0 => b"“c’†‘¾˜Y"
1 => b"“c’†‘¾"
2 => b"“c’†"
3 => b"“c"
4 => "?"
5 => b"?“c"

髙(はしごだか)以外の文字も含めて試しましたが、「UTF-8」から「Shift-JIS」に変換できない文字は「?」と出力される共通点がありました。

そこで下記のような実装をしてみました。
変換後の文字列$convertedValueに「?」が含まれていたら弾くという処理です。

class CanConvertUTF8ToShiftJIS implements ValidationRule
{
    public function validate(string $attribute, mixed $value, Closure $fail): void
    {		
        $convertedValue = mb_convert_encoding($value, "Shift-JIS", "UTF-8");

        if (strpos($convertedValue, "?") !== false) {
            $fail("$attribute contains characters that cannot be registerd.");
        }
    }
}

これなら大丈夫そうと思いましたが、1つ考慮漏れがありました・・・!

文字としての「?」自体は、「UTF-8」から「Shift-JIS」に変換が可能な文字になります。

なので入力値に最初から「?」が含まれる場合にmb_convert_encoding()の結果は下記のようになるので、これではバリデーションで弾かれてしまい、今回の要件を満たしてないということになってしまいます。

$value = "田中?郎"
$convertedValue = mb_convert_encoding($value , "Shift-JIS", "UTF-8")
=> b"“c’†?˜Y"

最終的な実装

上記の内容も踏まえ、最終的にはこのような実装となりました。
エンコードした値をデコードして、元の値と比較する方法になります。

class CanConvertUTF8ToShiftJIS implements ValidationRule
{
   public function validate(string $attribute, mixed $value, Closure $fail): void
   {		
       $shiftJis = mb_convert_encoding($value, "Shift-JIS", "UTF-8");
       $utf8 = mb_convert_encoding($shiftJis, "UTF-8", "Shift-JIS");
   
       if ($value !== $utf8) {
           $fail("$attribute contains characters that cannot be registerd.");
       }
   }
}

「UTF-8」から「Shift-JIS」に変換不可の場合

> $value = "髙田太郎" // 髙:はしごだかを含む
= "髙田太郎"

> $shiftJis = mb_convert_encoding($value, "Shift-JIS", "UTF-8")
= b"“c’†?˜Y"

> $utf8 = mb_convert_encoding($shiftJis, "UTF-8", "Shift-JIS")
= "?田太郎"

> $value === $utf8
= false

「UTF-8」から「Shift-JIS」に変換可能の場合

> $value = '田中?郎'
= "田中?郎"

> $shiftJis = mb_convert_encoding($value, 'Shift-JIS', 'UTF-8')
= b"?“c‘¾˜Y"

> $utf8 = mb_convert_encoding($shiftJis, 'UTF-8', 'Shift-JIS')
= "田中?郎"

> $value === $utf8
= true

さいごに

あまりないとは思いますが、php関数で期待する挙動ではない場合でも、このような組み合わせ方で要件を満たした実装をすることができました。

今回の最終的な実装に行き着くまでに、エンコーディング処理系のphp関数のいろいろな組み合わせを試しましたがなかなか期待する挙動にならず、かなり時間をかけてしまいましたが、実装にあたり親身にアドバイスをいただいた先輩方、改めてありがとうございました!🙏

Arsaga Developers Blog

Discussion