なぜ「Unicode 正規化」が実務で問題になるのか
同じ「見た目」の文字でも、内部的には“別の文字列”として扱われてしまうことがあります。
これが Unicode 正規化の話です。
例えば、次の 2 つは、見た目はどちらも「ガ」です。
- 「ガ」1 文字
- 「カ」+「゛」の 2 文字の組み合わせ
人間の目には同じですが、コンピュータ的には別物です。
このまま比較すると、=== では「違う」と判定されます。
検索でヒットしない。
ユニークキーの判定がすり抜ける。
「同じはずの文字列」が一致しない。
こういう“気持ち悪いバグ”を防ぐために、「Unicode 正規化」で
「同じ意味の文字列は、同じバイト列にそろえる」ということをします。
PHP で使うのは Normalizer(intl 拡張)
Normalizer::normalize の基本
PHP には intl 拡張があり、その中の Normalizer クラスで Unicode 正規化ができます。
use Normalizer;
$text = "ガ"; // 例
$normalized = Normalizer::normalize($text, Normalizer::FORM_C);
PHP第二引数の Normalizer::FORM_C が「どの正規化形式にするか」です。
よく使うのはこの 2 つだけ覚えておけば十分です。
FORM_C… NFC(合成済み)FORM_D… NFD(分解形)
実務では、まず「全部 NFC にそろえる」と決めてしまうのが一番シンプルです。
例題① 「ガ」が一致しない問題を正規化でそろえる
わざと「見た目だけ同じ」文字列を作る
// パターン A: 1 文字の「ガ」
$a = "ガ";
// パターン B: 「カ」 + 「゛」
$b = "カ゛";
var_dump($a === $b); // bool(false)
PHP見た目は同じでも、=== では false です。
正規化してから比較する
use Normalizer;
$aN = Normalizer::normalize($a, Normalizer::FORM_C);
$bN = Normalizer::normalize($b, Normalizer::FORM_C);
var_dump($aN === $bN); // bool(true)
PHPNFC にそろえることで、「同じ意味の文字列」が本当に同じバイト列になります。
検索や比較、ハッシュ化の前に「正規化しておく」ことで、こうした差異を吸収できます。
例題② ユーザー名・タグ名などを正規化して保存する
ユースケースのイメージ
ユーザー名やタグ名を DB のユニークキーにしているとします。
見た目は同じなのに、内部表現が違うせいで「重複登録」がすり抜けると困ります。
そこで、「保存前に必ず Unicode 正規化する」というルールを入れます。
実装例
use Normalizer;
function normalize_for_key(string $value): string
{
// まず Unicode 正規化(NFC)
$value = Normalizer::normalize($value, Normalizer::FORM_C);
// ついでに前後の空白もトリムしておく
return trim($value);
}
// 保存前
$username = $_POST['username'] ?? '';
$username = normalize_for_key($username);
// あとは $username をそのままユニークキーとして扱う
PHPここでの重要ポイントは、「“キーになる文字列”は必ず正規化してから使う」と決めてしまうことです。
これを徹底するだけで、「見た目は同じなのに別ユーザー扱い」という事故をかなり防げます。
どのタイミングで正規化するか
基本方針は「入口で正規化」
実務で一番扱いやすいのは、次のような方針です。
- 外部から入ってくる文字列(フォーム入力、API、CSV など)は、
「アプリに入った瞬間」に正規化してしまう。 - DB に保存するときも、保存前に正規化しておく。
- 比較・検索・ハッシュ化など、「同一性」が絡む処理の前には、
正規化済みであることを前提にする。
つまり、「中に入ってきた時点で、もう全部 NFC になっている世界」を作るイメージです。
そうしておけば、アプリ内部では「正規化されている前提」でシンプルに書けます。
まとめ:今日からの「Unicode 正規化」ユーティリティ
Unicode 正規化の本質は、「見た目が同じ文字列を、本当に同じバイト列にそろえる」ことです。
特に日本語+記号が混ざる環境では、「濁点付き」「結合文字」「コピー&ペースト由来の微妙な違い」が地味に効いてきます。
PHP では、Normalizer::normalize($str, Normalizer::FORM_C) を 1 本覚えておけば十分です。
実務ユーティリティとしては、例えばこんな関数を用意しておくと使いやすくなります。
use Normalizer;
function normalize_unicode_nfc(string $value): string
{
$normalized = Normalizer::normalize($value, Normalizer::FORM_C);
// 正規化に失敗した場合は、元の文字列をそのまま返す(お好みで)
return $normalized === false ? $value : $normalized;
}
PHPこれを「入力の入口」「保存前」「キーに使う前」に必ず通す——
それだけで、「なんでこれ一致しないの?」という謎バグをかなり減らせます。

