* @author Jun Nishikawa * @author Takako Miyagawa * @link http://www.netcommons.org NetCommons Project * @license http://www.netcommons.org/license.txt NetCommons License * @copyright Copyright 2014, NetCommons Project */ App::uses('Model', 'Model'); App::uses('ValidateMerge', 'NetCommons.Utility'); /** * NetCommonsApp Model * * CakePHPのModelクラスを継承してます。
* ドキュメントルートのapp.Mode.AppModelはこのクラスを継承しているので、
* 全モデルの基底クラスになります。
* Overrideしているメソッドもあり、CakePHPの通常動作と異なるものがありますので注意して下さい。 * * #### CakePHPのModel処理をOverrideしているメソッドです。 * [__construct](#method___construct)
* [getDataSource](#method_getDataSource)
* [create](#method_create)
* * @author Shohei Nakajima * @author Jun Nishikawa * @author Takako Miyagawa * @package NetCommons\NetCommons\Model * @SuppressWarnings(PHPMD.NumberOfChildren) * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) */ class NetCommonsAppModel extends Model { /** * use behaviors * * @var array */ public $actsAs = array( 'NetCommons.Trackable', 'NetCommons.NetCommonsValidationRule', ); /** * Validation rules * * @var array */ public $validate = array(); /** * 接続先DB master slave変更用 * * @var string */ private static $__changeDbConfig; /** * 接続先DB 古いslave保存用 * * @var string */ private static $__oldSlaveDbConfig; /** * Use database config * * @var string */ public $useDbConfig = 'master'; /** * ブロックId * Behaviorで関連データ削除する際に使用する * * @var string */ public $blockId = null; /** * ブロックKey * Behaviorで関連データ削除する際に使用する * * @var string */ public $blockKey = null; /** * コンテンツKey * Behaviorで関連データ削除する際に使用する * * @var string */ public $contentKey = null; /** * Constructor. DataSourceの選択 * * 接続先DBをランダムに選択します。 * * @param bool|int|string|array $id Set this ID for this model on startup, * can also be an array of options, see above. * @param string $table Name of database table to use. * @param string $ds DataSource connection name. * @return void * @SuppressWarnings(PHPMD.BooleanArgumentFlag) */ public function __construct($id = false, $table = null, $ds = null) { // If a datasource is set via params, use it and return if ((is_array($id) && isset($id['ds'])) || $ds) { parent::__construct($id, $table, $ds); return; } if (Hash::get((array)$id, 'testing') && $this->useDbConfig !== 'test') { $this->useDbConfig = 'test'; } // Use a static variable, to only use one connection per page-call (otherwise we would get a new handle every time a Model is created) static $_useDbConfig; if (!isset($_useDbConfig)) { $_useDbConfig = $this->__getRandomlySlave(); } $this->useDbConfig = $_useDbConfig; //actsAsをマージする $this->_mergeVars(['actsAs'], get_class(), false); parent::__construct($id, $table, $ds); } /** * Find the slaves we have * * @return string The name of the DataSource, as defined in app/Config/database.php */ private function __getRandomlySlave() { if ($this->useDbConfig === 'test') { return $this->useDbConfig; } //Master<->Slaveは全体的に見直しが必要。 //// Get all available database-configs //$sources = ConnectionManager::enumConnectionObjects(); // //// Find the slaves we have //$slaves = array(); //foreach ($sources as $name => $values) { // unset($values); // // Slaves have to be named "slave1", "slave2", etc... // if (preg_match('/^slave[0-9]+$/i', $name) == 1) { // $slaves[] = $name; // } //} // //// Randomly use a slave ////$dataSource = (count($slaves) !== 0) ? $slaves[rand(0, count($slaves) - 1)] : 'master'; $dataSource = 'master'; return $dataSource; } /** * Masterの接続先に変更する。 * * @return void */ public function setMasterDataSource() { self::$__changeDbConfig = 'master'; } /** * 以前のSlaveの接続先に戻す。なければ、ランダムに選択 * * @return void */ public function setSlaveDataSource() { if (self::$__oldSlaveDbConfig) { self::$__changeDbConfig = self::$__oldSlaveDbConfig; return; } self::$__changeDbConfig = $this->__getRandomlySlave(); } /** * Gets the DataSource to which this model is bound. * * @return DataSource A DataSource object */ public function getDataSource() { // MasterDBに切り替え処理 $this->__changeDataSource(); return parent::getDataSource(); } /** * MasterDBに切り替える処理 * * @return void */ private function __changeDataSource() { if (!empty(self::$__changeDbConfig) && self::$__changeDbConfig !== $this->useDbConfig && $this->useDbConfig !== 'test') { self::$__oldSlaveDbConfig = $this->useDbConfig; $this->useDbConfig = self::$__changeDbConfig; $this->_sourceConfigured = false; //self::$__changeDbConfig = null; $associations = Hash::merge( array_keys($this->hasOne), array_keys($this->hasMany), array_keys($this->belongsTo), array_keys($this->hasAndBelongsToMany) ); foreach ($associations as $btModelName) { $this->{$btModelName}->useDbConfig = $this->useDbConfig; if ($this->{$btModelName}->useTable !== false) { $this->{$btModelName}->setSource($this->{$btModelName}->useTable); } } } } /** * NetCommonsで使用する共通の値がセットされた結果を返します。
* CakePHPのSchemaは、not null指定されたカラムのdefaultがnullになっているため、
* ''(長さ0の文字列)に書き換えています。
* https://github.com/NetCommons3/NetCommons3/issues/7 * * #### セットされるデータ * ``` * 'room_id' => Current::read('Room.id'), * 'language_id' => Current::read('Language.id'), * 'block_id' => Current::read('Block.id'), * 'block_key' => Current::read('Block.key'), * 'frame_id' => Current::read('Frame.id'), * 'frame_key' => Current::read('Frame.key'), * 'plugin_key' => Inflector::underscore($this->plugin), * ``` * * @param bool|array $data Optional data array to assign to the model after it is created. If null or false, * schema data defaults are not merged. * @param bool $filterKey If true, overwrites any primary key input with an empty value * * @return array The current Model::data; after merging $data and/or defaults from database * @link http://book.cakephp.org/2.0/en/models/saving-your-data.html#model-create-array-data-array * @SuppressWarnings(PHPMD.BooleanArgumentFlag) */ public function create($data = array(), $filterKey = false) { if ($data !== null && $data !== false && $this->useTable !== false) { if (empty($data[$this->alias])) { $data = $this->_setAliasData($data); } $options = $this->_getDefaultValue(); $data = Hash::merge($options, $data); } return parent::create($data, $filterKey); } /** * Initializes the model for writing a new record, loading the default values * for those fields that are not defined in $data, and clearing previous validation errors. * Especially helpful for saving data in loops. * * @param bool|array $data Optional data array to assign to the model after it is created. If null or false, * schema data defaults are not merged. * @param bool $filterKey If true, overwrites any primary key input with an empty value * * @return array The current Model::data; after merging $data and/or defaults from database * @link http://book.cakephp.org/2.0/en/models/saving-your-data.html#model-create-array-data-array * @SuppressWarnings(PHPMD.BooleanArgumentFlag) */ public function createAll($data = array(), $filterKey = false) { $newRecord = $this->create($data, $filterKey); $associations = Hash::merge( array_keys($this->hasOne), array_keys($this->belongsTo) ); foreach ($associations as $model) { if ($model === 'TrackableCreator' || $model === 'TrackableUpdater') { continue; } if (isset($data[$model])) { $options = $data[$model]; } else { $options = array(); } $newRecord = Hash::merge($newRecord, $this->$model->create($options, $filterKey)); } return $newRecord; } /** * transaction begin * * @return void */ public function begin() { $this->setMasterDataSource(); $dataSource = $this->getDataSource(); $dataSource->begin(); } /** * transaction commit * * @return void */ public function commit() { $dataSource = $this->getDataSource(); $dataSource->commit(); } /** * transaction rollback * * @param mixed $ex Exception * @return void * @throws Exception */ public function rollback($ex = null) { $dataSource = $this->getDataSource(); $dataSource->rollback(); if ($ex) { //if ($this->useDbConfig !== 'test') { // CakeLog::error($ex); //} throw $ex; } } /** * Load models * * @param array $models models to load * @return void */ public function loadModels(array $models = []) { foreach ($models as $model => $class) { $this->$model = ClassRegistry::init([ 'class' => $class, 'testing' => ($this->useDbConfig === 'test') ], true); if (is_object($this->$model)) { if ($this->$model->useDbConfig === 'test' && $this->useDbConfig !== 'test') { $this->setDataSource($this->$model->useDbConfig); } } //if ($this->$model->useDbConfig !== 'test') { // $this->$model->setDataSource($source); //} } } /** * 全カラムのデフォルト値をセットした配列を返す。 * * @return array デフォルト値をセットした配列 */ protected function _getDefaultValue() { $options = array(); $currents = array(); if (class_exists('Current')) { $currents = array( 'room_id' => Current::read('Room.id'), 'language_id' => Current::read('Language.id'), 'block_id' => Current::read('Block.id'), 'block_key' => Current::read('Block.key'), 'frame_id' => Current::read('Frame.id'), 'frame_key' => Current::read('Frame.key'), 'plugin_key' => Inflector::underscore($this->plugin), ); } foreach ($this->schema() as $fieldName => $fieldDetail) { if ($fieldName !== $this->primaryKey) { if (($fieldDetail['null'] === false) && ($fieldDetail['default'] === null)) { // not nullカラムのdefault指定がなかったら空文字にしておく。 // @see https://github.com/NetCommons3/NetCommons3/issues/7 $options[$fieldName] = ''; } else { $options[$fieldName] = $fieldDetail['default']; } } // Currentの値をセット if (isset($currents[$fieldName])) { $options[$fieldName] = $currents[$fieldName]; } } $options = $this->_setAliasData($options); return $options; } /** * cakephpの登録処理をオーバーライド * * cakephpのsave時にcreated,modifiedの項目にセットする時間が、date_default_timezone_setに影響してしまうため、 * 登録直前にUTCに変更し、登録後にdate_default_timezone_setを元に戻すため、オーバライドする * * @param array $data Data to save. * @param array $options can have following keys: * * - validate: Set to true/false to enable or disable validation. * - fieldList: An array of fields you want to allow for saving. * - callbacks: Set to false to disable callbacks. Using 'before' or 'after' * will enable only those callbacks. * - `counterCache`: Boolean to control updating of counter caches (if any) * * @return mixed On success Model::$data if its not empty or true, false on failure * @throws PDOException * @link https://book.cakephp.org/2.0/en/models/saving-your-data.html */ protected function _doSave($data = null, $options = array()) { $nowDefaultTz = date_default_timezone_get(); date_default_timezone_set('UTC'); $result = parent::_doSave($data, $options); date_default_timezone_set($nowDefaultTz); return $result; } /** * Findの結果をキャッシュする際、afterFindは実行させたいので、CakeのModelを継承して、カスタマイズする。 * * @param string $type Type of find operation (all / first / count / neighbors / list / threaded) * @param array $query Option fields (conditions / fields / joins / limit / offset / order / page / group / callbacks) * @return array * @see Model::_readDataSource() */ protected function _readDataSource($type, $query) { if (! $this->Behaviors->enabled('NetCommons.NetCommonsCache') || ! isset($query['cacheKey'])) { //キャッシュを使用しない場合、親のメソッドを呼ぶ return parent::_readDataSource($type, $query); } else { //キャッシュを使用する場合、キャッシュの有無を確かめ、 //キャッシュがあれば、キャッシュの内容を使用し、無ければ、DBから取得する。 //その後の処理は、親と同じ処理。 $results = $this->cacheRead($type, $query['cacheKey']); if (! $results) { $results = $this->getDataSource()->read($this, $query); $this->cacheWrite($results, $type, $query['cacheKey']); } $this->resetAssociations(); if ($query['callbacks'] === true || $query['callbacks'] === 'after') { $results = $this->_filterResults($results); } $this->findQueryType = null; if ($this->findMethods[$type] === true) { return $this->{'_find' . ucfirst($type)}('after', $query, $results); } } } }