44
39

More than 5 years have passed since last update.

【CakePHP3】 `Type` の話

Last updated at Posted at 2016-11-11

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

ココらへんの話をします

  • Table、Entityと Typeについて
  • DBから引っ張ってきた値がPHPのデータに変換されること、その逆にPHPのデータがDBにwrite可能な形に変換されること
  • 独自Typeの作成

Entity のpropertyは、どのように決定される(生成される)か?

例えばこんなケースはどうでしょうか。

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

$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

なぜ createdFrozenTimeになっているのか

結論から言うと、 CakePHPのデフォルト設定として 「datetime型カラムを、FrozenTimeとして扱う」というように紐付けられています。
この紐付けが Type という仕組みです。
cf) データの型

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

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

というフローになります

Column <=> Typeの紐付け

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

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

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

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

  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) 独自の型を作成する

toPHP()

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

// 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らせる」事を意味します。

// 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では「与えられた値をそのまま保持する」ようになっています。

// 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を例に用いると、それぞれ次のようになります。

// 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;
}
// 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;
}

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

// 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);
}
// 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 となっている点に注意して下さい。

こちらについてはコアコード内での実装例が少なく、またbookでの例 が明瞭だったのでそちらに説明を譲ります。
(MySQL等の)Database関数を用いた処理を行う際などに利用されます。

独自Typeの作成

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

独自Typeの読み込み・紐付け

独自に作成したTypeをアプリケーションから簡単に呼び出すために、「定義済みTypeの登録」を行います。
この仕事を担うのがType::map()です。
例えばphp serializedな文字列を扱うApp\Database\Type\PhpSerializedTypeを作成し、serializedという名前で登録するのであれば

Type::map('serialized', 'App\Database\Type\PhpSerializedType');

となります。
これをconfig/boostrap.phpなど適切な箇所に記述して下さい。

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

protected function _initializeSchema(Schema $schema)
{
     parent::_initializeSchema($schema);
     $schema->columnType('data', 'serialized');
     return $schema;
}
44
39
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
44
39