PHP開発における文字コードの罠

PHPで開発をしていると、**見た目には同じ文字列なのに比較結果がおかしい**という現象に
遭遇することがあります。
例えば、「_（アンダースコア）」に見える文字が実は別のエンコーディング/コードポイントで
格納されており、`$str1 == $str2`が`false`になってしまう…といった具合です。

この記事では、PHPにおける文字列比較と文字コードの罠について、
実例や対処法を交えて解説します。

## PHPの文字列比較とエンコーディングの問題

まず押さえておきたいのは、**PHPの文字列比較はバイナリレベルで行われる**という点です。
PHPの文字列型は単なるバイト列であり、文字コード（エンコーディング）の情報は
持ちません。

そのため、たとえ人間から見て同じ「文字」に見えても、内部のバイト列が異なれば比較結果は一致しません。


```php
$a = "é";           // 合成済みの e アキュート (U+00E9)
$b = "é";           // e + 結合アキュート記号 (U+0065 U+0301)

var_dump($a == $b);                 // bool(false)
var_dump(strlen($a), strlen($b));   // int(2), int(3)
var_dump(bin2hex($a), bin2hex($b)); // "c3a9", "65cc81"
```

見た目は同じでも、バイト列が異なるため不一致になります。

## 【実例】筆者が遭遇した罠

新卒時代、PHPでとあるCSV処理を書いていたとき、以下のような比較に失敗してハマりました。

```php
if (preg_match('/ /', $text)) {
  // 本来ここに入るはずが通らない
}
```

CSVデータを調べた結果、` `（半角スペース） に
見えていたのは ` `（ノーブレークスペース）だったのです！
前後のロジック自体に問題があると勘違いをして処理の見直しに
大きく時間を使うことになりました。
非常に紛らわしい....

## 起こりやすい文字の誤認識・変換の例

### 全角と半角の違い
- `_`（U+005F）と `＿`（U+FF3F）
- `-`（U+002D）と `−`（U+2212）など

### ダッシュ類や長音記号
- `-`、`–`、`―`、`ー`などはフォント次第で非常に紛らわしい

### 波ダッシュとチルダ
- `〜`（U+301C）と `～`（U+FF5E）は似ているが別物

### 円マークとバックスラッシュ
- `¥`（U+00A5）と `\`（U+005C）

### 合成文字（濁点付き）
- `ガ` = `カ(U+30AB)` + `゙(U+3099)` または `ガ(U+30AC)`

## 解決方法まとめ
Webアプリであれば、データベース、HTMLテンプレート、PHP内部処理を
すべて「UTF-8」で統一するのが一般的ですので
（近年のPHPは`default_charset`もUTF-8がデフォルト）。

文字コードの不一致による問題を防ぐには、入力から出力まで一貫した
エンコーディングを使うのが理想です。

しかし、現実には外部から様々な文字コードのデータが入ってきたり
過去のシステムとの連携で「Shift_JIS」を扱わざるを得なかったりします。

そこで、文字コードの違いを扱う際に役立つ確認・変換・正規化の方法をまとめます。

### 1. 文字コードの確認
`mb_detect_encoding($str, $enc_list, $strict)` 関数で文字コードを推定できます。
下記のようにしておけば、与えた文字列が配列指定内のどれなのかをチェックできます。
```php
$enc = mb_detect_encoding($str, ['UTF-8','SJIS','EUC-JP','ASCII'], true);
```
第3引数の `$strict` を `true` にすると厳密判定となるが、誤検出リスクもあるので注意が必要。

### 2. 意図したエンコーディングに変換する
`mb_convert_encoding($str, $to, $from)`関数（もしくはiconv関数）で
文字エンコーディングを変換できます。

例えば外部から来たSJISの文字列をUTF-8に統一してから比較したり、
逆に出力を全てSJISに変換してからファイル保存する、といった対応が可能です。
文字コードがはっきり分かっているなら$fromに明示し、不明な場合は
```php
mb_convert_encoding($str, 'UTF-8', 'auto')
```
のように自動判定に任せることもできます​
もっとも、**auto判定**は誤検出の可能性もあるため可能なら事前に判定しておくか

```php
mb_convert_encoding($str, 'UTF-8', ['ASCII','JIS','UTF-8','SJIS']);
```

このように配列指定するほうが安全です。

変換時に変換不能な文字（ターゲットの文字コードに存在しない文字）が含まれると、「?」や〓に置換されたり、エラーになったりします。 

必要に応じて**IGNOREオプション**を付けて無視させたり、別の近い文字に置き換える処理を入れることも検討してください。

### 3. Unicode正規化
濁点の分離問題のように、Unicode内部の表現揺れが原因の場合は`Normalizer` クラスが有効です。
ext-intl拡張がインストールされていれば組み込みの`Normalizer` クラスが利用できます。
例えば、文字列をNFC（合成）形式に正規化したい場合は
```php
$normalized = Normalizer::normalize($str, Normalizer::FORM_C);
```
のようにします。
これで「カ+゙」のような分解文字列も「ガ」の単一コードポイント列に正規化されます。

正規化処理には内部で大きなテーブルを参照する必要があるため多少コストはかかりますが、
比較前に一度だけ正規化しておけばその後の文字列比較はスムーズです。

### 4. 全角・半角の統一
ユーザー入力などで全角と半角の表記ゆれが問題になる場合、PHPの`mb_convert_kana()` 関数が便利です。
オプション指定で変換内容を制御できます​。
```php
mb_convert_kana($str, 'asKV');
```

## デバッグ時のポイント（見えない違いを見抜くには）

- バイト値やコードポイントを確認する
- `strlen()` と `mb_strlen()` の違いを見る
- `var_dump()` で予期せぬ改行や空白をチェック
- `hexdump` コマンドやエディタのバイナリ表示機能を活用

ポイントは、文字列を「目に見えるまま信じない」ことです。
常に内部のコード値に着目し、必要なら自分でそれを確認する作業を挟みましょう。

## おわりに

文字コードの違いによるPHPでの比較の罠について、代表的な例と対処法を解説しました。
何気なく使っている文字にも、歴史的経緯や環境依存で思わぬ異なるコードが割り当てられているものです。

幸いPHPにはマルチバイト文字を扱うための関数群（mb_*系）や
国際化対応の拡張など、対策手段がひととおり揃っています。

ポイントをしっかり押さえておけば、
「見た目は同じなのになぜか比較が通らない…」という沼にはまることも減るでしょう。 

文字コードの不一致は一種の「罠」ですが、逆に言えばきちんと
エンコーディングを統一・確認する習慣さえつければ回避可能な問題です。

筆者自身も過去にこの罠では痛い目を見ましたが、
それ以来「入出力はUTF-8に統一」「怪しい時はバイト確認」を徹底するようにしています。

「PHP開発における文字コードの罠」を踏まないように、
そして仮に踏んでしまっても冷静にデバッグできるようになれば幸いです。

---

### 参考リンク
- [PHP: マルチバイト文字列  - Manual](https://www.php.net/manual/ja/book.mbstring.php)
- [PHP: Normalizer - Manual](https://www.php.net/manual/ja/class.normalizer.php)
- [PHP: サポート - Manual](https://www.php.net/manual/ja/mbstring.supported-encodings.php)
- [文字化け・文字コードの解説（Qiita）](https://qiita.com/tags/文字コード)