コンテンツにスキップ

モデル - Model_Baseクラス

ここでは、モデルの基底クラスについて説明いたします

なぜフルスクラッチで書いたのか

Section titled “なぜフルスクラッチで書いたのか”

FuelPHPには、Model_CrudOrm\Modelといったモデルクラスが提供されています

しかし、Model_Baseは、それらのクラスを継承しておりません

その理由をまず説明いたします

Model_CrudOrm\Modelは、

プロパティの取得や設定など、高頻度でアクセスされるプロパティにマジックメソッド(__get, __set)を使用しています

マジックメソッドは動作が遅い1ため、コードではなく「動作速度を」軽量にしたいことと、

後述のいくつかの制約や機能を加えたかったため、Model_CrudもOrmパッケージも継承しない基底クラスModel_Baseを実装しました

完全にゼロベースではなく、FuelのModel_Crudにあるメソッドを一部組み込んでおります


Model_Baseクラスは、

モデルクラスの名前とテーブル名の関係に命名規約があるなど、いくつか制約を加えています

1. モデルクラス名とテーブル名の命名規約

Section titled “1. モデルクラス名とテーブル名の命名規約”

Model_Baseを継承するクラスの名前と、DB上のテーブル名は対応させなければなりません

代わりに、命名規約に則りさえすれば、楽にモデルのコードを書くことができます

テーブル名とモデルクラス名の関係は、

  • テーブル名:複数形
  • モデルクラス名:単数形

とする必要がありますいくつか具体例を挙げます

テーブル名単数形モデルクラス名
usersuserModel_User
companiescompanyModel_Company
peoplepersonModel_Person

この対応関係を作るために、内部でFuelPHPのInflector::pluralizeメソッド2を使用しています

単数形の単語を与えると、複数形にした単語を返すメソッドです

そのため、oil consoleコマンドで手軽に対応関係を確認することができます

Terminal window
$ 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. プロパティ名とカラム名の命名規約

Section titled “2. プロパティ名とカラム名の命名規約”

DBのカラム名と、モデルクラスのプロパティ名にも命名規約があります

カラム名とプロパティ名は一致させる
Section titled “カラム名とプロパティ名は一致させる”

モデルの設定や、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へ
Section titled “保存処理から除外したいプロパティは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に存在するカラムとモデルのみに存在するプロパティを分ける
Section titled “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は、主キーを用いて検索を行います

引数 mixed $id 取得したいモデルのIDを指定(文字列か数値を想定) 戻り値 指定されたIDに対応するデータが有れば、自身のインスタンス、なければnull

Terminal window
$model = Model_Test::find(1); // id値で検索
$exist = is_null($model) ? 'ない' : 'ある';

引数 なし 戻り値 モデルクラスと対応するテーブル内の全件のデータ

Terminal window
$models = Model_Test::findAll(); // 全件取得
$found = (count($models) > 0) ? '1件以上ある' : '1件もない'

findBy($column, $value, $operator = ’=’)

Section titled “findBy($column, $value, $operator = ’=’)”

引数 string $column 比較するカラム名 mixed $value 比較する値 string $operator 比較に用いる演算子(デフォルトは=) 戻り値 比較条件にマッチしたモデルのインスタンスの配列、1件もマッチしなければ空配列

Terminal window
$models = Model_Test::findBy('hoge', 'foo'); // カラムhogeの値がfooなモデルを取得
$models = Model_Test::findBy('age' 10, '>='); // 比較>=を使用
$found = (count($models) > 0) ? '1件以上ある' : '1件もない'

引数 string $column 検索に使用するカラム mixed $value 検索に使用する値 戻り値 比較条件にマッチしたモデルのインスタンスの配列、1件もマッチしなければ空配列

Terminal window
$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

Terminal window
$row_count = Model_Test::count(); // テーブルの全行数をカウント

保存系の処理には、insert, update, save, createを用意しています

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になり、キー重複の例外が発生しない

引数 なし 戻り値 更新に成功したらtrue、更新に失敗したらfalse

// DBから取得したデータならupdateを用いる
$model = Model_Test::find(1);
$model->content = 'rewrite from PHP';
$model->update(); // DBの値が更新される

引数 なし 戻り値 保存に成功すれば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メソッドは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メソッドは物理削除を行います

論理削除は今のところ実装していません

引数 なし 戻り値 削除に成功すればtrue、失敗したらfalse

Terminal window
$models = Model_Test::findAll();
foreach($models as $model) {
$model->delete(); // DBから物理削除する(findAllと組み合わせて全件削除)
}

大量のデータを外部から取得し、素早く一度にDBへ挿入する際に便利ですが、

現状、実装する予定はありません


ネクストエンジンアプリ基盤ではcreated_atのデフォルト値にCURRENT_TIMESTAMP

updated_atのデフォルト値にCURRENT_TIMESTAMP on UPDATE CURRENT_TIMESTAMPが指定されています

これらの指定が前提となっているため、ネクストエンジンアプリ基盤(PHPコード)からこれらのカラムについて一切操作を行いません

ここで気をつけるべき事項は、

既存のDBの内容と全く同じ内容でupdateを実行した場合、成功はするがupdated_atは更新されないことです

何か値を変更して更新した場合にはupdated_atは更新されます

この挙動にご注意下さい

insert, update, save時のプロパティの更新

Section titled “insert, update, save時のプロパティの更新”

挿入や保存をした際に、

DBにはその日時が反映されていますが、

モデルのプロパティのcreated_atupdated_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_atupdated_atを処理に使用したい場合には、挿入および保存の挙動にご注意下さい

これら日時に関するプロパティを更新したいケースのためにフックを用意しています

そちらに日付更新の処理を実装して下さい


各種DBとの通信するメソッドの前後に挟める各種フックを用意しています

フック名フックが呼び出されるタイミングメソッドに渡される引数
before_savesave(), insert(), update()before_save()
after_savesave(), insert(), update()after_save(boolean $success)
before_insertinsert()before_insert()
after_insertinsert()after_insert(boolean $success)
before_updateupdate()before_update()
after_updateupdate()after_update(boolean $success)
before_deletedelete()before_delete()
after_deletedelete()after_delete(boolean $success)
before_validatevalidate()before_validate()
after_validatevalidate()after_validate(boolean $success)
after_findfind(), 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 ↩

はじめに ↩