2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

CakePHPAdvent Calendar 2019

Day 16

独自の型(Type)を作ってJsonTypeを拡張する話

Last updated at Posted at 2019-12-16

この記事は 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エスケープされた状態で保存されてしまいました。

_MySQL_5_7_28__127_0_0_1_qa_app_qa_app_posts.png

これは、デフォルトの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.保存して動作確認

それではここまでの内容を反映させて保存してみましょう。

_MySQL_5_7_28__127_0_0_1_qa_app_qa_app_posts.png

無事にbody_jsonカラムがUnicodeエスケープされていないことが確認できました!

おわりに

今回は物凄くシンプルな例でしたが、アプリケーションによって独自のTypeを定義してあげることでコードの見通しがグンと良くなることがあります。ぜひTypeを使ってより良いコードを書いてみてください!

  1. そもそも、カラムの型がJSON型(MySQLなら5.7から使える)であれば、本記事の対応をしなくてもDBに保存される値はUnicodeエスケープされないようでした。ドキュメントを確認したのですが、なぜそうなるのかはっきりとした仕様が分からなかったのでご存知の方がいましたら共有していただけると嬉しいです!

  2. が、本記事では特殊な事情によりText型のカラムにJSONを保存しているケースのために対応を記載します。

2
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?