CakePHP3 配列からEntityを生成する!

どうもこんばんは

はじまりました。CakePHP3 Advent Calendar 2016

生意気にも毎週水曜日を私、Junkinsの日として3本記事を書かせていただきたいと思います。 読んでいただけると幸いです。

これを機にブログも始めました。 Junkinsのブログです。 よろしくお願いいたします。

1本目の記事のテーマは「配列 -> Entity編」です。 配列をEntityに変換する処理についてまとめたいと思います。

このテーマを選んだ理由は、CakePHP2経験者が最も戸惑う部分がEntityの扱いだと思うからです。

1. 配列からEntityの第一歩

リクエストデータをEntityに変換する処理はEntityを生成する処理の中でもよく見る処理です。

こんな感じですかね。

// newEntity()
$data = $this->request->data;
$topic = $this->Topics->newEntity($data);

// patchEntity()
$topic = $this->Topics->newEntity();
$data = $this->request->data;
$topic = $this->Topics->patchEntity($topic, $data);

2. 実際にEntityを生成する処理はどこに記述されているか?

newEntity()とpatchEntity()はCake\ORM\Tableの関数です。 しかし、実際に配列からEntityの生成を行っているクラスはCake\ORM\Marshallerになります。

コードに記述する関数 実際にEntityの生成を行う関数
newEntity() Marshaller::one()
patchEntity() Marshaller::merge()

3. Marshallerの流れを見てみよう

Marshaller::one()、Marshaller::merge()の処理の流れ

この流れを掴んでデバックがやりやすくなったなと感じます。

  1. 事前のコールバック処理(beforeMarshal)
  2. EntityClassの生成 ( newEntity()の場合のみ )
  3. アソシエーション先のModelのEntity生成と結合
  4. バリデーションの実行
  5. カラムの型に合わせたデータの変換
  6. Entityと配列のマージ ( patchEntity()のみ )

※ 直接、Entityをインスタンス化する場合では、Entityのaliasは自動で設定されませんが、 Marshallerを通すとaliasが自動で付与されます。

4. それぞれの処理を詳しく見てみよう。

1. 事前のコールバック処理(beforeMarshal)

このコールバックはCakePHP2でいう、beforeValidate()になります。 バリデーションの前に対象データを触ることができます。

使いかたはこのリンクを参照してください。 http://book.cakephp.org/3.0/ja/orm/saving-data.html#before-marshal

※ CakePHP2にはafterValidate()がありましたが、afterMarshal()はありません。

2. EntityClassの生成

newEntity()の場合のみEntityのインスタンス化をおこないます。 第一引数を特に指定しない場合はTableクラス内で処理が完結します。

// Tableクラスで処理が完結する
$this->Topics->newEntity();

// Marshaller::one()が実行される
$data = $this->request->data;
$this->Topics->newEntity($data);
3. アソシエーション先のModelのEntity生成と結合

アソシエーション先のModelのEntityの生成はMarshallerで行なわれます。

直接Entityクラスをインスタンス化する場合ではアソシエーション先のModelのEntityは結合されません。

アソシエーションの設定を行うのはTableクラスなので、考えれば当然ですね。

4. バリデーションの実行

バリデーションの実行場所はMarshaller::_validate()です。 下記にソースコードを書きます。

バリデーションの実行場所はTableクラスとばかり思ってましたが、Marshaller::_validate()を経由して実行されます。

Table->Marshaller->Tableと処理が移動するので、ちょっと複雑で読みにくい部分ですね。

protected function _validate($data, $options, $isNew)
    {
        if (!$options['validate']) {
            return [];
        }
        if ($options['validate'] === true) {
            $options['validate'] = $this->_table->validator('default');
        }
        if (is_string($options['validate'])) {
            $options['validate'] = $this->_table->validator($options['validate']);
        }
        if (!is_object($options['validate'])) {
            throw new RuntimeException(
                sprintf('validate must be a boolean, a string or an object. Got %s.', gettype($options['validate']))
            );
        }

        return $options['validate']->errors($data, $isNew);
    }
5. カラムの型に合わせたデータの変換

CakePHP3では配列からEntityを生成する際に、そのカラムのデータ型に合わせた値の変換をかけます。

デフォルトで基本的なTypeクラス (Cake\Database\Type) は定義されていますが、独自のカラムタイプを定義することも可能です。

http://book.cakephp.org/3.0/en/orm/database-basics.html#adding-custom-types

バリデーションを無事通過したデータはこのTypeクラスを経由して変換がかけられます。

CakePHP2のときは配列を持ち回していたので、最初は日時の文字列が Cake\I18n\FrozenTimeオブジェクトにいきなり変わってびっくりしました。

6. Entityと配列のマージ ( patchEntity()のみ )

そして、最終的に既存Entityとのマージを行います。 既存のEntityデータはoriginalに、変更があったデータはdirtyに格納されます。

5. Junkinsの悩み

直接Entityをインスタンス化する場合は下記のオプションが指定できるのですが、 Marshallerを通してインスタンス化する場合はオプションの指定ができません。。。。。

        $options += [
            'useSetters' => true,
            'markClean' => false,
            'markNew' => null,
            'guard' => false,
            'source' => null
        ];

現状は、設定を書いたEntityクラスをTableに設定してインスタンス化を行っていますが。。。

もっと楽な方法ないかなー。