モデル - Model_Baseクラス
ここでは、モデルの基底クラスについて説明いたします
なぜフルスクラッチで書いたのか
FuelPHPには、Model_Crud
やOrm\Model
といったモデルクラスが提供されています
しかし、Model_Base
は、それらのクラスを継承しておりません
その理由をまず説明いたします
Model_Crud
やOrm\Model
は、
プロパティの取得や設定など、高頻度でアクセスされるプロパティにマジックメソッド(__get
, __set
)を使用しています
マジックメソッドは動作が遅い1ため、コードではなく「動作速度を」軽量にしたいことと、
後述のいくつかの制約や機能を加えたかったため、Model_CrudもOrmパッケージも継承しない基底クラスModel_Baseを実装しました
完全にゼロベースではなく、FuelのModel_Crudにあるメソッドを一部組み込んでおります
制約
Model_Baseクラスは、
モデルクラスの名前とテーブル名の関係に命名規約があるなど、いくつか制約を加えています
1. モデルクラス名とテーブル名の命名規約
Model_Baseを継承するクラスの名前と、DB上のテーブル名は対応させなければなりません
代わりに、命名規約に則りさえすれば、楽にモデルのコードを書くことができます
テーブル名とモデルクラス名の関係は、
- テーブル名:複数形
- モデルクラス名:単数形
とする必要がありますいくつか具体例を挙げます
テーブル名 | 単数形 | モデルクラス名 |
---|---|---|
users | user | Model_User |
companies | company | Model_Company |
people | person | Model_Person |
この対応関係を作るために、内部でFuelPHPのInflector::pluralize
メソッド2を使用しています
単数形の単語を与えると、複数形にした単語を返すメソッドです
そのため、oil console
コマンドで手軽に対応関係を確認することができます
$ cd /path/to/sample-fuelphp
$ php oil console
Fuel 1.7.1 - PHP 5.4.28 (cli) (Jun 19 2014 14:31:57) [Darwin][]
>>> Inflector::pluralize('man')
men # この場合テーブル名は`Model_Men`とする
>>> Inflector::pluralize('monky')
monkies # この場合テーブル名は`Model_Monkies`とする
>>> Inflector::pluralize('person')
people # この場合テーブル名は`Model_People`とする
しかし、oil generate model
コマンドで生成されたマイグレーションが生成するテーブルは、
モデルクラス名の複数形になるため、さほど意識する必要はありません
※ Moel_Baseクラス自身をインスタンス化する必要がないため、abstractにしています
2. プロパティ名とカラム名の命名規約
DBのカラム名と、モデルクラスのプロパティ名にも命名規約があります
カラム名とプロパティ名は一致させる
モデルの設定や、Model_Baseの継承クラスの処理を簡潔するための規約です
DBのカラム名と、モデルクラスのプロパティ名は一致させる必要があります
また、モデルで定義されたプロパティは、public
で、かつDBに存在する必要があります
class Model_User extends Model_Base {
public $company_id = null;
public $uid = null;
public $next_engine_id = null;
public $email = null;
public $access_token = null;
public $refresh_token = null;
}
例えば、はじめに3で説明したusers
テーブルを使用したModel_User
の場合ですと、
以下の様な定義になります
なお、id
, created_at
, updated_at
はModel_Baseに定義されているため、再定義する必要はありません
これら3つのカラムは、全てのテーブルが持っていると制約しています
しかし、全てのプロパティが強制的に保存されてしまうと柔軟性に欠けるため、
保存対象から除外するカラムを指定する設定を設けています
保存処理から除外したいプロパティはignoreSaveKey
へ
保存対象から除外するカラムを指定する、ignoreSaveKey
というプロパティがあります
class Model_Hoge extends Model_Base {
public $hoge = null;
public $foo = null;
public $bar = null;
protected $ignoreSaveKey = array('hoge', 'foo');
}
$model = new Model_Hoge();
$model->bar = 'fizz';
// hogeとfooは除外指定されているため、barのみが保存される
$model->save(); // INSERT INTO `hoges` SET `bar`='fizz'
上記のように、プロパティ名の配列を指定します
- DBに存在しないカラムをプロパティとして使用したい、
- PHPからDBに保存したくないプロパティ
などを保存対象から除外することができます
DBに存在するカラムとモデルのみに存在するプロパティを分ける
DBに存在しないカラムをプロパティとして定義することができるため、
第三者がコードを見た際に、DBに存在するプロパティなのか見間違うリスクが生まれました
そのため、DBに保存するカラム名(プロパティ名)と、
DBに保存しない、モデルクラスにのみ記述されたプロパティ名を分けるための命名規約を設けます
種類 | 命名規則 |
---|---|
DBに保存するプロパティ | スネークケース(hoge, hoge_foo_bar) |
モデルにのみ存在するプロパティ | キャメルケース(hoge, hogeFooBar) |
上記の表のように、命名を分けて下さい
Model_Baseで言えば、id
, created_at
, updated_at
がカラム名(スネークケース)validationErrors
, primaryKey
, ignoreSaveKey
がモデルのプロパティ(キャメルケース)です
主キーの変更
主キーは、primaryKey
プロパティで指定します
デフォルトではid
と指定されていますが、任意の値へ変更することができます
// 主キーを変更するには$primaryKeyプロパティの値を変更する
class Model_Map extends Model_Base {
public $foo_id = null;
public $hoge_id = null;
protected static $primaryKey = 'foo_id';
}
// idではなく、カラムfoo_idの値で検索をかけることができる
$map = Model_Map::find('hoge-foo-bar');
現状、主キーは単一の指定のみ対応しております
複合主キーには対応しておりません
基本的な機能
次は、Model_Baseで使える基本的な機能について説明いたします
※ このドキュメントには、細かな仕様を記述しておりません
細かい挙動や仕様については、fuel/app/classes/model/base.php
を御覧下さい
インスタンスの生成
__construct(array $data = array())
- 引数
array
$data プロパティに与える初期値を連想配列で指定
class Model_Test extends Model_Base {
public $hoge = null;
}
$model = new Model_Test();
$model->hoge; // null
$model = new Model_Test(array('hoge' => 1));
$model->hoge; // 1
読み取り
データの読み取りには、find
, findAll
, findBy
, findLike
を用意しています
find($id)
findは、主キーを用いて検索を行います
- 引数
- mixed $id 取得したいモデルのIDを指定(文字列か数値を想定)
- 戻り値
- 指定されたIDに対応するデータが有れば、自身のインスタンス、なければnull
$model = Model_Test::find(1); // id値で検索
$exist = is_null($model) ? 'ない' : 'ある';
findAll()
- 引数
- なし
- 戻り値
- モデルクラスと対応するテーブル内の全件のデータ
$models = Model_Test::findAll(); // 全件取得
$found = (count($models) > 0) ? '1件以上ある' : '1件もない'
findBy($column, $value, $operator = '=')
- 引数
- string $column 比較するカラム名
- mixed $value 比較する値
- string $operator 比較に用いる演算子(デフォルトは
=
) - 戻り値
- 比較条件にマッチしたモデルのインスタンスの配列、1件もマッチしなければ空配列
$models = Model_Test::findBy('hoge', 'foo'); // カラムhogeの値がfooなモデルを取得
$models = Model_Test::findBy('age' 10, '>='); // 比較に>=を使用
$found = (count($models) > 0) ? '1件以上ある' : '1件もない'
findLike($column, $value)
- 引数
- string $column 検索に使用するカラム
- mixed $value 検索に使用する値
- 戻り値
- 比較条件にマッチしたモデルのインスタンスの配列、1件もマッチしなければ空配列
$models = Model_Test::findLike('description', 'Geek'); // LIKEを使用して部分一致を検索
$found = (count($models) > 0) ? '1件以上ある' : '1件もない'
上記のような、汎用的で簡素な検索・取得メソッドしか用意しておりません
複雑な検索クエリを書かなければならない場合は、検索用のメソッドを実装して下さい
また、件数のカウントにはcount
を用意しています
countについてはModel_Crudのcountメソッドをそのまま移植しています
使い方はそちらを御覧下さい
Model_Crud - クラス - FuelPHP ドキュメント
http://fuelphp.jp/docs/1.7/classes/model_crud/methods.html#/method_count
$row_count = Model_Test::count(); // テーブルの全行数をカウント
保存処理
保存系の処理には、insert
, update
, save
, create
を用意しています
insert($insert_ignore = false)
insertメソッドは、第一引数にINSERT IGNORE...
を使用するか否かのフラグを受け取ります
しかし、INSERT IGNOREを使用するとINSERTに使用する処理時間が若干増えます
デフォルトの動作はなるべく早く軽くしておきたいため、引数を省略した場合は通常のINSERTを行います
- 引数
- boolean $insert_ignore trueが渡されるとINSERT IGNORE句を使用する(デフォルトは使用しない)
- 戻り値
- 挿入に成功すればtrue
挿入に失敗(INSERT IGNOREで挿入が起きなかった場合も)したらfalse
また、保存に成功した場合、挿入されたidがidプロパティに反映されます
// まだDBに未挿入のデータはinsertを用いる
$model = new Model_Test(array('hoge' => 'foo'));
$model->insert(); // DBに挿入される
$model->id; // 挿入されたIDが格納されている
$model->insert(); // そのまま再INSERTしようとするとキー重複で例外発生
$model->insert(true); // trueを渡すとINSERT IGNOREになり、キー重複の例外が発生しない
update()
- 引数
- なし
- 戻り値
- 更新に成功したらtrue、更新に失敗したらfalse
// DBから取得したデータならupdateを用いる
$model = Model_Test::find(1);
$model->content = 'rewrite from PHP';
$model->update(); // DBの値が更新される
save()
- 引数
- なし
- 戻り値
- 保存に成功すればtrue、失敗すればfalse
saveメソッドはINSERT ... ON DUPLICATE KEY UPDATE ...
を使用します
DBにUNIQUE等の制約がかかっているカラムがあるとしたときに、
重複する値がDBに存在している場合は、モデルの値でDBを更新します
重複する値がDBに存在していない場合は、挿入を行います
// fuel/packages/nextengine/classes/nextengine/api/client/router.phpの内部
private function _createUser($company_id) {
$user_info = parent::apiExecute('/api_v1_login_user/info');
$user_info = $user_info['data'][0];
$user = new \Model_User();
$user->company_id = $company_id;
$user->uid = $user_info['uid'];
$user->next_engine_id = $user_info['pic_ne_id'];
$user->email = $user_info['pic_mail_address'];
$user->access_token = $this->_access_token;
$user->refresh_token = $this->_refresh_token;
$user->created_at = \DB::expr('NOW()');
$user->save(); // INSERT or UPDATE
return $user;
}
create(array $columns, array $values) {
create
メソッドはstaticメソッドとして提供されており、
引数に特定の形式で配列を与えると、DBに挿入を行い、そのデータをモデルクラスのインスタンスとして返します
- 引数
- string[] $columns 挿入するカラム名の配列
- array[] $values 値の配列、指定する順序は$columnsの順序と対応している必要がある
- 戻り値
- 挿入されたデータのインスタンスの配列
Model_Test::create(
array(
'username',
'password',
),
array(
array('hogehoge', 'hugahuga', 'foooooooooo'), // usernameに挿入する値の配列
array('foobar', 'hige', 'fizzbuzz'), // passwordに挿入する値の配列
));
削除処理
削除処理は、delete
を用意しています
delete()
deleteメソッドは物理削除を行います
論理削除は今のところ実装していません
- 引数
- なし
- 戻り値
- 削除に成功すればtrue、失敗したらfalse
$models = Model_Test::findAll();
foreach($models as $model) {
$model->delete(); // DBから物理削除する(findAllと組み合わせて全件削除)
}
バルクインサート
大量のデータを外部から取得し、素早く一度にDBへ挿入する際に便利ですが、
現状、実装する予定はありません
モデルを扱う上での注意点
created_atとupdated_atについて
MySQLの仕様
ネクストエンジンアプリ基盤ではcreated_at
のデフォルト値にCURRENT_TIMESTAMP
、updated_at
のデフォルト値にCURRENT_TIMESTAMP on UPDATE CURRENT_TIMESTAMP
が指定されています
これらの指定が前提となっているため、ネクストエンジンアプリ基盤(PHPコード)からこれらのカラムについて一切操作を行いません
ここで気をつけるべき事項は、
既存のDBの内容と全く同じ内容でupdateを実行した場合、成功はするがupdated_at
は更新されないことです
何か値を変更して更新した場合にはupdated_atは更新されます
この挙動にご注意下さい
insert, update, save時のプロパティの更新
挿入や保存をした際に、
DBにはその日時が反映されていますが、
モデルのプロパティのcreated_at
とupdated_at
は更新されません
これらの情報をモデルのプロパティにも反映させたい場合、
挿入や更新後に、一度SELECT文を発行せざるを得ないため、パフォーマンスの観点から実装していません
具体的なコードを上げます
<?php
$model = Model_Hoge::find(1);
$model->hoge = 1;
$model->save();
$model->updated_at; // 更新されてない
/////////////////////////////////////////
$model = new Model_Hoge();
$model->hoge = 1;
$model->save();
$model->created_at; // 更新されてない
$model->updated_at; // 更新されてない
created_at
やupdated_at
を処理に使用したい場合には、挿入および保存の挙動にご注意下さい
これら日時に関するプロパティを更新したいケースのためにフックを用意しています
そちらに日付更新の処理を実装して下さい
フック
各種DBとの通信するメソッドの前後に挟める各種フックを用意しています
フック名 | フックが呼び出されるタイミング | メソッドに渡される引数 |
---|---|---|
before_save | save() , insert() , update() |
before_save() |
after_save | save() , insert() , update() |
after_save(boolean $success ) |
before_insert | insert() |
before_insert() |
after_insert | insert() |
after_insert(boolean $success ) |
before_update | update() |
before_update() |
after_update | update() |
after_update(boolean $success ) |
before_delete | delete() |
before_delete() |
after_delete | delete() |
after_delete(boolean $success ) |
before_validate | validate() |
before_validate() |
after_validate | validate() |
after_validate(boolean $success ) |
after_find | find() , findAll() , findBy() , findLike() |
after_find(array $record ) |
ほぼすべてのメソッドがインスタンスメソッドです
メソッド内部で$this
を用いればインスタンスにアクセスすることができます
ですが、find系のメソッドは全てstaticメソッドです
そのためafter_findのみstaticメソッドとなっていますご注意下さい
具体的なコードを挙げます
class Model_Admin extends Model_Base {
public $username;
public $password;
// 挿入前にパスワードをハッシュ化
// @override
public function before_insert($query) {
$this->password = password_hash($this->password);
}
// ユーザ名とパスワードを渡し、それにマッチするユーザが居ればtrueを、いなければfalseを返す
public static function verify($username, $raw_password) {
$admin = self::findBy('username', $username);
if(is_null($admin)) return false;
$admin = $admin[0];
if(password_verify($raw_password, $admin->password)) {
return true;
} else {
return false;
}
}
}
更新履歴
- 2015/02/17: モデル - Model_Baseクラスページ作成
-
Inflector - クラス - FuelPHP ドキュメント
http://fuelphp.jp/docs/1.7/classes/inflector.html#/method_pluralize ↩