💾

Laravel で json をDBに保存した時にUnicodeエスケープされる

2023/09/11に公開

Eloquent での json の使い方

(環境: PHP 8.1以上、Laravel 10)
LaravelのEloquent(Model) で json を扱う時は、モデルに以下のように記述と思います。
casts

入出力する際に json を encode/decode することなく扱えます。
配列をそのまま保存
casts 便利ですね。

非常に便利ですが、罠があります。
環境によって unicode にエスケープされたりされなかったりするのです。
unicodeエスケープされたデータ

どういう時困るか

実はPHPで通常利用する上では特に困りません。
しかしDB検索時の条件に指定する時などに困ります。

たとえば files テーブルの tags というカラムに配列でタグを入れていくという仕様になっていたとします。
そしてそのタグを条件に検索したい時、以下のようなSQLを発行すると思います。
select * files where tags LIKE "%景色%";

ところが、DB上では \u666f\u8272 というようにユニコードエスケープされた状態で保存されているので、文字列が不一致として認識されます。
これだとタグ検索ができません。

解法1: 検索する時にUnicodeエスケープする

シンプルです。
しかし環境によってエスケープされないこともあるので、両方に対応するコードを記述する必要があります。

File::where(function ($query) {
    $query->where('tags', 'LIKE', '%' . $escaped . '%')
        ->orWhere('tags', 'LIKE', '%' . $unescaped . '%');
});

環境によって不要なコードが含まれているので冗長ですし、DBに無駄な負荷がかかります。
また php は標準で ユニコードにエスケープする関数がないので、自前で用意しなければなりません。

解法2: json_encode 時にエスケープしない

Eloquent で json を cast しているのは Illuminate\Database\Eloquent\Casts\Json というクラスです。
この中で通常は json_encode が呼び出されています。

public static function encode(mixed $value): mixed
{
    return isset(static::$encoder) ? (static::$encoder)($value) : json_encode($value);
}

コードをよく見ると static::$encoder があれば、それを用いてエンコード処理してくれるので、どうやらここを使えばうまくいけそうです。

さて json_encode は第2引数でフラグを設定することができます。
そして JSON_UNESCAPED_UNICODE という、名前の通りユニコードのエスケープをさせないためのフラグが定義されています。

これらを用い App\Providers\AppServiceProviderboot 関数内で以下のように記述してあげます。

public function boot(): void
{
    Json::encodeUsing(fn ($value) => json_encode($value, JSON_UNESCAPED_UNICODE));
}

これで DB に保存する時にマルチバイトがユニコードエスケープされないようになります。

注記

  • その他にも casts を使わずに自分で encode/decode する方法もあり
  • そもそも json を検索しない工夫をする (アプリケーションの規模やコストに合わせ)
  • fn () => ...アロー関数
  • LIKE 検索するときはきちんとSQLインジェクション対策をしましょう
  • JSON の定義済み定数

Discussion