この記事は CakePHP Advent Calendar 2019 16日目の記事です。
やりたいこと
jsonを保存しているカラムにデータを保存するときに、Unicodeエスケープしないようにしたい1
前提
本記事は CakePHP3のType
とは何なのか、理解していることを前提とします。
よく分からない、という方は以下の記事がおすすめなのでぜひ読んで理解してみてください。
本記事に関連する箇所だけまとめると以下のとおりです。
- CakePHP3では
Type
と呼ばれる仕組みを使って、各カラムの値をアプリケーション内でどのように扱うかを決定している - データベースから取得するとき、またその逆に保存するときの「データの加工」は
Type
の責務となっている - stringやinteger、jsonなど基本的な型の
Type
はデフォルトで提供されている
デフォルトのJsonTypeだとUnicodeエスケープされる
前提として、環境はPHP7.2、CakePHP 3.8.7、MySQL5.7を使用します。
また、テーブルスキーマは以下の通りです(jsonを保存するカラムはbody_json
)。2
CREATE TABLE `posts` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`body` varchar(255) NOT NULL,
`body_json` text NOT NULL,
`created` datetime NOT NULL,
`modified` datetime NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4;
まずはカラムとTypeの紐付けをしましょう。
PostsTableで以下のように指定します。
protected function _initializeSchema(TableSchema $schema)
{
$schema->setColumnType('body_json', 'json'); // body_jsonカラムをJsonTypeに設定
return $schema;
}
この環境でデータを保存してみます。
すると以下のようにbody_jsonカラムにはUnicodeエスケープされた状態で保存されてしまいました。
これは、デフォルトのJsonTypeのjson_encode()処理が以下のようにオプションが未指定になっているためです。あえてエスケープをしないためには JSON_UNESCAPED_UNICODE
を第2引数で指定する必要があります。
public function toDatabase($value, Driver $driver)
{
if (is_resource($value)) {
throw new InvalidArgumentException('Cannot convert a resource value to JSON');
}
return json_encode($value);
}
というわけで、JsonTypeの json_encode()
部分でオプションを指定するために、JsonTypeを拡張した独自の型を作ってアプリケーション内で利用してみましょう。
JsonTypeを継承してUnicodeエスケープしない独自のJsonTypeを定義する
独自の型を定義する方法は独自の型を作成するに簡単な説明が書いてあるのでこの流れにそって作っていきます。
1.TypeInterfaceを実装したクラスを用意する
独自の型を定義するにはCake\Database\TypeInterfaceを実装したクラスを作成する必要があります。
実運用ではCake\Database\Typeを継承したクラスを作るのが一番簡単でしょう。
今回はJsonTypeの一部分の処理を変えるだけなので、更に手を抜いて JsonType を継承して独自のJsonTypeを作っていきます。
独自のJsonTypeでやることはこの行でオプションにJSON_UNESCAPED_UNICODE
を指定するだけです。
早速、src/Database/Type/
ディレクトリを作成し、ファイルを作成します。名前は UnescapedUnicodeJsonType
としましょう。コードは以下のとおりです。
<?php
namespace App\Database\Type;
use Cake\Database\Driver;
use Cake\Database\Type\JsonType;
use InvalidArgumentException;
class UnescapedUnicodeJsonType extends JsonType
{
protected $_name = 'unescaped_unicode_json';
/**
* 入力された値をJSON文字列に変換する
*
* @param mixed $value エンコードする値. resource型以外を指定する
* @param Driver $driver データベースドライバ
* @return string|null JSONエンコードされた文字列、またはnull
*/
public function toDatabase($value, Driver $driver)
{
if (is_resource($value)) {
throw new InvalidArgumentException('resource型はJSONに変換できません');
}
// Unicode文字をエスケープしないためオプションを指定
return json_encode($value, JSON_UNESCAPED_UNICODE);
}
}
toDatabase()
内の処理としてはオプションを指定した以外は親クラスと同じです。
$_name
は型をアプリケーション内から特定するための名前です。coreのコードを見ても複数単語をつなぐパターンがなかったのでここではスネークケースで記述しています。
独自に拡張した部分についてはテストも作っておきましょう。こちらも親クラスのテストコードを参考に実装してください。
/**
* toDatabase()のテスト
*
* @return void
*/
public function testToDatabase()
{
/* (略) */
$this->assertSame(
json_encode(['a' => 'b'], JSON_UNESCAPED_UNICODE),
$this->type->toDatabase(['a' => 'b'], $this->driver),
'配列の変形が適切にできていない'
);
$this->assertSame(
json_encode(['キー' => 'バリュー'], JSON_UNESCAPED_UNICODE),
$this->type->toDatabase(['キー' => 'バリュー'], $this->driver),
'マルチバイト文字を含む配列の変形が適切にできていない'
);
}
2.アプリケーションから使えるようにする
新しい型を作成したら、"型マッピング"に追加する必要があります。ここでは、bootstrap.phpに以下の処理を追加します。
Type::setMap(array_merge(
Type::getMap(),
['unescaped_unicode_json' => UnescapedUnicodeJsonType::class] // ここで追加したいTypeを指定
));
最後に、このTypeと利用したいカラムとを紐付けます。ここではPostsTable.phpで紐付ける処理を追加します。
protected function _initializeSchema(TableSchema $schema)
{
parent::_initializeSchema($schema);
$schema->setColumnType('body_json', 'unescaped_unicode_json');
return $schema;
}
3.保存して動作確認
それではここまでの内容を反映させて保存してみましょう。
無事にbody_json
カラムがUnicodeエスケープされていないことが確認できました!
おわりに
今回は物凄くシンプルな例でしたが、アプリケーションによって独自のTypeを定義してあげることでコードの見通しがグンと良くなることがあります。ぜひTypeを使ってより良いコードを書いてみてください!