社内用にまとめた内容の転載。
ココらへんの話をします
- 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
なぜ created
が FrozenTime
になっているのか
結論から言うと、 CakePHPのデフォルト設定として 「datetime型カラムを、FrozenTime
として扱う」というように紐付けられています。
この紐付けが Type という仕組みです。
cf) データの型
ここではさらっと書かれていますが、
Database(datetime型)
-> ORMがロード
-> Typeの指定に従い、CakePHPが触る型に変換 (FrozenDate)
-> Entityのpropertyとしてセット
というフローになります
- ※ FrozenTimeについては次の記事を参照して下さい。
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処理をするという一手間を意識せずに省略することができます。
もう少しだけ実態のイメージを持つための補足を加えると、
- Databaseから引っ張ってきたデータを
- Tableクラスの保持しているスキーマ情報(ここにTypeを含む)に従って
- 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;
}