【CakePHP3】 `Type` の話

社内用にまとめた内容の転載。

### ココらへんの話をします
* Table、Entityと Typeについて
* DBから引っ張ってきた値がPHPのデータに変換されること、その逆にPHPのデータがDBにwrite可能な形に変換されること
* 独自Typeの作成

### `Entity` のpropertyは、どのように決定される(生成される)か？
例えばこんなケースはどうでしょうか。

**例)「Userが登録した年を表示する」**

```php
$user = $this->Users->get(1);
$reggedYear = $user->created->year;
```
もう少し詳細に見てみると、 `User::$created` はこのようなデータになっています

```
// $created =
object(Cake\I18n\FrozenTime) {
	'time' => '2014-05-02T06:09:17+09:00',
	'timezone' => 'Asia/Tokyo',
	'fixedNowTime' => false
}
```
ここで議論すべきは、この`\Cake\l18n\FrozenTime` がどこから来て、いつ生成されているのか？という点です。
cf) [Class Cake\\I18n\\FrozenTime \| CakePHP](http://api.cakephp.org/3.3/class-Cake.I18n.FrozenTime.html)

#### なぜ `created`が `FrozenTime`になっているのか
結論から言うと、 CakePHPのデフォルト設定として 「datetime型カラムを、`FrozenTime`として扱う」というように紐付けられています。
この紐付けが **Type** という仕組みです。
cf) [データの型](http://book.cakephp.org/3.0/ja/orm/database-basics.html#id8)

ここではさらっと書かれていますが、

```
Database(datetime型) 
-> ORMがロード 
-> Typeの指定に従い、CakePHPが触る型に変換 (FrozenDate)
-> Entityのpropertyとしてセット
```
というフローになります

* ※ FrozenTimeについては次の記事を参照して下さい。
    * [不変な日付と時刻](http://book.cakephp.org/3.0/ja/core-libraries/time.html)
    * [新しいイミュータブルな Date と Time オブジェクト](http://book.cakephp.org/3.0/ja/appendices/3-2-migration-guide.html)
    * [イミュータブル DateTime オブジェクトの有効化](http://book.cakephp.org/3.0/ja/orm/database-basics.html#datetime)

#### Column <=> Typeの紐付け
原則として、(前項で挙げている)デフォルトの紐付け設定のまま利用するのであれば「各カラムに対応するTypeはどこで指定されているのか」を意識する必要はありません。
しかしながら、(当然のように)アプリケーション側から独自の紐付け設定を行う・もしくは上書きすることが可能です。

これはTable内で行われます。
以下の例は、「Database的には`可変長文字列`に過ぎないが、内容として構造データ(json)を保持しているカラム」を扱いやすくするための実装です。

```php
namespace App\Model\Table;

use Cake\Database\Schema\Table as Schema;

class SpecialSales extends Table
{
   protected function _initializeSchema(Schema $schema)
   {
        parent::_initializeSchema($schema);
        $schema->columnType('data', 'json');
        return $schema;
   }
}
```
これにより、「`$SpecialSales->data`は、アプリケーション側からアクセスした時点で既にjson_decode済み=(単なる文字列ではなく)Object化されている」事が保証されました。実装者は、明示的にdecode処理をするという一手間を意識せずに省略することができます。

cf) [Schema\\Table Objects](http://book.cakephp.org/3.0/en/orm/schema-system.html)

もう少しだけ実態のイメージを持つための補足を加えると、

1. Databaseから引っ張ってきたデータを
2. Tableクラスの保持しているスキーマ情報(ここにTypeを含む)に従って
3. Entityを吐き出す

という流れになります。
そのため、「Entityを生成するのに必要な情報はTableが持っている」のです。

### Database <> PHP間の変換
では、先の例で挙げたような「(PDO的には)日時型文字列にすぎないものを、Date系拡張クラスのインスタンス化処理を施す」「(PDO的には)json-serialized文字列にすぎないものを、json_decode()を施したObjectに変換する」といった処理というのは、どの段で行われているのでしょうか。
また、PHPのオブジェクトを保存する = Databaseに投げ込む際の変換はどうでしょうか。

その仕事を担う正体が `Type` になります。
Databaseと(Cake)PHPの橋渡し的存在です。
大まかに言って、次の記事が最も入門的な解説を行っています。

cf) [独自の型を作成する](http://book.cakephp.org/3.0/ja/orm/database-basics.html#adding-custom-database-types)

#### toPHP()
まず、コレが「Databaseから引っ張ってきた値をPHPのクラス(や値)に変換する」仕事を行っている正体です。
例として、次は「TypeとしてBooleanを指定された場合に、フェッチ直後に通る処理」です。

```php
// https://github.com/cakephp/cakephp/blob/master/src/Database/Type/BoolType.php#L68
public function toPHP($value, Driver $driver)
{
    if ($value === null) {
        return null;
    }
    if (is_string($value) && !is_numeric($value)) {
        return strtolower($value) === 'true' ? true : false;
    }
    return !empty($value);
}
```   
標準で梱包されているTypeを見る限り、(PDOの)返り値がNULLだった場合に(各種インスタンスや有効値ではなく)NULLを返すというのがお作法になります。
その後、`(string)true`と同値でなければfalseを返すし、数値や数字文字列であれば「`== 0`以外はFALSE」と判定しています。
このように、「引っ張ってきた値をbooleanとして解釈する」処理が施されるのです。

#### marshal()
こちらも「PHPの値に変換する」働きを持つので `toPHP()`と混同しがちなのですが、これは「ユーザーからrequestされたデータやアプリケーション側が集約してきたデータを投入し、Entityオブジェクトを生成する(or 更新する)」際の処理になります。
すなわち、 `$Table->newEntity($data)` だったり `$Table->patchEntity($entity, $data)`などを実行する際にフックされます。

次は `IntegerType` の例で、「整数値として解釈可能であればintにキャストした値を、そうでなければnullを」返すように処理を施しています。
すなわち、「Entityのpropertyとして整数値が保持される、あるいは不適合ならnullらせる」事を意味します。

```php
// https://github.com/cakephp/cakephp/blob/master/src/Database/Type/IntegerType.php#L79
public function marshal($value)
{
    if ($value === null || $value === '') {
        return null;
    }
    if (is_numeric($value) || ctype_digit($value)) {
        return (int)$value;
    }
    if (is_array($value)) {
        return 1;
    }
    return null;
}
```

他方で、興味深いことに`JsonType`では「与えられた値をそのまま保持する」ようになっています。

```php
// https://github.com/cakephp/cakephp/blob/master/src/Database/Type/JsonType.php#L72
    /**
     * Marshalls request data into a JSON compatible structure.
     *
     * @param mixed $value The value to convert.
     * @return mixed Converted value.
     */
    public function marshal($value)
    {
        return $value;
    }
    
```

これは、「Databaseに突っ込む」もしくは「返り値をユーザーに(=ブラウザに)表示する」までは **何かのインスタンスだろうと配列だろうと、直前まで生データのまま扱える** 状態を保持していることになります。便利。

#### toDatabase, toStatement, toExpression
最後が `toDatabase()` / `toStatement()`, `toExpression()` の3つです。
これらは全て「PHPデータをDatabaseに突っ込む」際に呼ばれ、各driverに喰わせるデータへと変換を行います。

##### toDatabase + toStatement
`toDatabase`は「Databaseに挿入するvalue」を、`toStatement`は「Databaseに通知するtype」を指定するメソッドです。
再度`IntegerType`を例に用いると、それぞれ次のようになります。

```php
// https://github.com/cakephp/cakephp/blob/df1b9b8aa75ece32dbe5e5343a249f3058ba1f3f/src/Database/Type/IntegerType.php#L31
/**
 * Convert integer data into the database format.
 *
 * @param mixed $value The value to convert.
 * @param \Cake\Database\Driver $driver The driver instance to convert with.
 * @return int
 */
public function toDatabase($value, Driver $driver)
{
    if ($value === null || $value === '') {
        return null;
    }
    if (!is_scalar($value)) {
        throw new InvalidArgumentException('Cannot convert value to integer');
    }
    return (int)$value;
}
```

```php
// https://github.com/cakephp/cakephp/blob/df1b9b8aa75ece32dbe5e5343a249f3058ba1f3f/src/Database/Type/IntegerType.php#L67
/**
 * Get the correct PDO binding type for integer data.
 *
 * @param mixed $value The value being bound.
 * @param \Cake\Database\Driver $driver The driver.
 * @return int
 */
public function toStatement($value, Driver $driver)
{
    return PDO::PARAM_INT;
}
```

これが実際に呼ばれているのは、ドライバー内の実装になります。


```php
// https://github.com/cakephp/cakephp/blob/df1b9b8aa75ece32dbe5e5343a249f3058ba1f3f/src/Database/Statement/PDOStatement.php#L64
// @see 
public function bindValue($column, $value, $type = 'string')
{
    if ($type === null) {
        $type = 'string';
    }
    if (!ctype_digit($type)) {
        list($value, $type) = $this->cast($value, $type);
    }
    $this->_statement->bindValue($column, $value, $type);
}
```

```php
// https://github.com/cakephp/cakephp/blob/df1b9b8aa75ece32dbe5e5343a249f3058ba1f3f/src/Database/TypeConverterTrait.php#L31
/**
 * Converts a give value to a suitable database value based on type
 * and return relevant internal statement type
 *
 * @param mixed $value The value to cast
 * @param \Cake\Database\Type|string $type The type name or type instance to use.
 * @return array list containing converted value and internal type
 */
public function cast($value, $type)
{
    if (is_string($type)) {
        $type = Type::build($type);
    }
    if ($type instanceof TypeInterface) {
        $value = $type->toDatabase($value, $this->_driver);
        $type = $type->toStatement($value, $this->_driver);
    }
    return [$value, $type];
}
```

##### toExpression
前二者とは経路が事なり、「実際に利用されるステートメントを生成する」のが`toExpression` になります。
こちらは由来するInterfaceが異なり、 [ExpressionTypeInterface](https://github.com/cakephp/cakephp/blob/df1b9b8aa75ece32dbe5e5343a249f3058ba1f3f/src/Database/Type/ExpressionTypeInterface.php) となっている点に注意して下さい。

こちらについてはコアコード内での実装例が少なく、また[bookでの例](http://book.cakephp.org/3.0/ja/orm/database-basics.html#sql) が明瞭だったのでそちらに説明を譲ります。
(MySQL等の)Database関数を用いた処理を行う際などに利用されます。

### 独自Typeの作成
前章の内容が理解されているのであれば、独自のTypeを実装するのはとても簡単な仕事です。
`App\Database\Type`以下に、  `HogeType`というクラスを設置しましょう。
このクラスには、 [`Cake\Database\Type\ExpressionTypeInterface`](https://github.com/cakephp/cakephp/blob/df1b9b8aa75ece32dbe5e5343a249f3058ba1f3f/src/Database/TypeInterface.php) を実装する必要がありますが、基本的には [`Cake\Database\Type`](https://github.com/cakephp/cakephp/blob/df1b9b8aa75ece32dbe5e5343a249f3058ba1f3f/src/Database/Type.php) を継承してしまえば、必要最低限のメソッドの実装だけで済みます。
例えば、コアコードを覗いてみると [UuidType](https://github.com/cakephp/cakephp/blob/df1b9b8aa75ece32dbe5e5343a249f3058ba1f3f/src/Database/Type/UuidType.php) はStringTypeを親に持っており、コメントを除けば数行程度の実装量しかありません。

#### 独自Typeの読み込み・紐付け
独自に作成したTypeをアプリケーションから簡単に呼び出すために、「定義済みTypeの登録」を行います。
この仕事を担うのが[Type::map()](http://api.cakephp.org/3.3/class-Cake.Database.Type.html#_map)です。
例えばphp serializedな文字列を扱う`App\Database\Type\PhpSerializedType`を作成し、`serialized`という名前で登録するのであれば

```php
Type::map('serialized', 'App\Database\Type\PhpSerializedType');
```
となります。
これを`config/boostrap.php`など適切な箇所に記述して下さい。

その上で、各Tableクラスの`initializeschema()`メソッド内で対応fieldに対して紐付けを行います。(先述の内容)

```php
protected function _initializeSchema(Schema $schema)
{
     parent::_initializeSchema($schema);
     $schema->columnType('data', 'serialized');
     return $schema;
}
```