* @link http://www.yiiframework.com/forum/index.php/forum/13-extensions/
* @copyright Copyright © 2012 Vitalii Tron
* @license New BSD License
* @version 0.7 alpha 2 $Id: CachedModelBehavior.php 1 2012-09-27 12:29:24Z vittron $
* @package application.components
* @since 1.1.10
* @access public
*/
class CachedModelBehavior extends CActiveRecordBehavior
{
const CACHE_KEY_PREFIX = 'YiiCachedModel.';
const DEFAULT_CACHE_DURATION = 300;
const DEFAULT_SYNC_INTERVAL = 60;
const FIELD_MAX_LENGTH = 200;
const INSERTS_PER_COMMAND = 1000;
/**
* @var integer the value defines how long to hold this data in cache. You can
* define this property during behavior attaching or in application params
* (.../config/main.php):
*
* 'params'=>array(
* // this is used in contact page
* 'adminEmail'=>'webmaster@example.com',
* ...
* 'cachedModelCacheDuration' => 900,
* ),
*
* Defaults to null.
*/
public $cacheDuration;
/**
* @var string cache identificator.
* Defaults to 'cache'.
*/
public $cacheID = 'cache';
/**
* @var boolean whether to store new models in cache and implement packet insert
* during synchronization. This allows to make multiple inserts in one sql
* command, which alleviates DB server load. Note, that all model
* attributes should be included to cache. Note, that very often inserts,
* or big cached model size, or too large syncronization interval may lead to
* cache storage overflow, if this option enabled.
* Defaults to false.
*/
public $delayedInsert = false;
/**
* @var array fields, which are NOT stored in cache. It is recommended to
* exclude large data fields (BLOB, TEXT and derivative field types; and also
* VARCHAR, CHAR, BINARY, VARBINARY fields with length more than
* {@link CachedModelBehavior::FIELD_MAX_LENGTH}. It is also recommended to
* exclude fields with user important information (passwords, payment
* transactions etc). Note, that you can define either
* {@link CachedModelBehavior::excludedFields} or
* {@link CachedModelBehavior::fields}, but not both. You can also define this
* value as array "array('story', 'description', 'password', ...)" or as a
* string "'story,description, password , ...'".
* Defaults to empty array.
*/
public $excludedFields = array();
/**
* @var array fields, which are stored in cache. It is recommended to
* cache small data (INT, FLOAT types, small strings); also frequently
* requested/updated attributes with not so significatn data. If you leave
* this property not defined, all model attributes will be included to cache.
* Note, that you can define either
* {@link CachedModelBehavior::excludedFields} or
* {@link CachedModelBehavior::fields}, but not both. You can also define this
* value as array "array('id', 'counter1', 'counter2', ...)" or as a string
* "'id,counter1, counter2 , ...'".
* Defaults to empty array.
*/
public $fields = array();
/**
* @var boolean whether behavior is enabled. For example, when you use
* {@link save()} method:
*
* $model = new CActiveRecord;
* $model->someAttr = 'hello world';
* ...
* $model->cm()->save();
*
* So, if {@link isEnabled} is true, {@link CachedModelBehavior::save()} method
* will be called and if false or there are no attributes to cache,
* {@link CActiveRecord::save()} method will be called.
* Defaults to true.
*/
public $isEnabled = true;
/**
* @var integer the value defines time interval between synchronization cache
* and main DB storage. You can define this property during behavior attaching
* or in application params (.../config/main.php):
*
* 'params'=>array(
* // this is used in contact page
* 'adminEmail'=>'webmaster@example.com',
* ...
* 'cachedModelSyncInterval' => 900,
* ),
*
* Defaults to null.
*/
public $syncInterval;
private $_attrs; // array copy of model-owner attributes
private $_cache; // ICache cache storage
private $_cachedModel; // array clone of data, stored in cache for this model
private $_cachedModelId; // string id in cache for model data
private $_changedModelsId; // string id in cache for list of models,
// which was changed after last synchronization
private $_cols; // array list of columns in db table
private $_id; // string|array model identificator
private $_insertModelsId; // string id in cache for list of models (including model data),
// which wait insert to main DB
private $_keyField; // string|array primary key for this model
private $_lastSync; // integer timestamp of last synchronization (was it successfull or not)
private $_lastSyncId; // string id in cache for last synchronization timestamp
private $_rels; // array relations with other models. May be useful for future development.
private $_systemAttrs; // array default system (hidden) attributes for every model, copied to cache
private $_table; // string table name
/**
* Initializes the component instance.
* This method expands the parent implementation by checking if cache is available
* and many other important assignments and checkings.
* @param CActiveRecord $owner owner of this behavior
* @throws CException if owner is not CActiveRecord or inherited class instance
* @throws CException if {@link _cache} is not ICache instance
* @throws CException if there are mistakes in {@link fields} or {@link excludedFields}
* properties, or if both fields are not empty
* @throws CException if {@link syncInterval} more than {@link cacheDuration}
*/
public function attach($owner)
{
parent::attach($owner);
// $this->_cachedModel is loaded later
if (!($this->owner instanceof CActiveRecord))
throw new CException(Yii::t('yii','You should attach CacheModelBehavior only to CActiveRecord or inherited class instanses. Please make sure you attach this component to a valid class.'));
if (empty($this->_cols))
$this->_cols = $this->owner->getMetaData()->columns;
if (empty($this->_attrs))
$this->_attrs = $this->owner->attributes;
$this->fields = $this->processFields($this->fields);
$this->excludedFields = $this->processFields($this->excludedFields);
if (!$this->isAnyCached())
return false;
$this->_cache=Yii::app()->getComponent($this->cacheID);
if(!($this->_cache instanceof ICache))
throw new CException(Yii::t('yii','CacheModelBehavior.cacheID is invalid. Please make sure "{id}" refers to a valid cache application component.',
array('{id}'=>$this->cacheID)));
$mData = $this->owner->getMetaData();
$this->_keyField = $mData->tableSchema->primaryKey;
$this->_table = $mData->tableSchema->name;
$this->_rels = $mData->relations;
// it is possible to choose either included or excluded fields, but not both
if (!empty($this->fields) && !empty($this->excludedFields))
throw new CException(Yii::t('cachedModel', 'You can define either included or excluded fields'));
if ($this->syncInterval === null)
{
if (isset(Yii::app()->params['cachedModelSyncInterval']))
$this->syncInterval = Yii::app()->params['cachedModelSyncInterval'];
else
$this->syncInterval = self::DEFAULT_SYNC_INTERVAL;
}
if ($this->cacheDuration === null)
{
if (isset(Yii::app()->params['cachedModelCacheDuration']))
$this->cacheDuration = Yii::app()->params['cachedModelCacheDuration'];
else
$this->cacheDuration = self::DEFAULT_CACHE_DURATION;
}
if ($this->syncInterval >= $this->cacheDuration)
throw new CException(Yii::t('CachedModelBehavior CacheDuration property should be more than SyncInterval property'));
if ($this->cacheDuration == 0)
Yii::log(Yii::t('cachedModel', 'It is dangerous to set cache data duration "0", which means "never expire", for class "{class}". It can lead to cache storage overflow.', array(
'{class}' => get_class($this->owner),
)), 'warning');
// default system attributes for cached model
$this->_systemAttrs = array(
'_isChanged' => false,
'_id' => $this->owner->getIsNewRecord() ? '' : $this->getModelId(),
'_class' => get_class($this->owner),
'_lastChangeTime' => 0,
'_lastSyncTime' => 0,
'_changedFields' => array(),
);
$this->showLongFieldsWarnings();
}
/**
* This method is invoked before deleting a record.
*/
public function beforeDelete($event)
{
}
/**
* This method is invoked before an AR finder executes a find call.
*/
public function beforeFind($event)
{
}
/**
* This method is invoked before saving a record (after validation, if any).
*
* Terminates model insert to DB table, if {@link delayedInsert} is true and
* all model attributes are cached. Model is inserted into cache then
* and will be inserted into DB table during nearest synchronization.
*
* Terminates model update in DB table, if all changed attributes are cached.
* Model is changed in cache then and will be changed in DB table during nearest
* sychronization.
* @see {@link CActiveRecord::beforeSave()}
*/
public function beforeSave($event)
{
if (!$this->isEnabled)
return false;
if (!$this->isAnyCached())
return false;
if ($this->isAllCached())
{
$this->init();
if ($this->owner->getIsNewRecord())
{
if ($this->delayedInsert)
{
$this->addInsertModel();
$event->isValid = false;
}
}
else
{
$this->updateCachedModel();
$this->saveCachedModel();
$event->isValid = false;
}
}
}
/**
* This method is invoked after each record is instantiated by a find method.
* Store model in cache after find, if cached model doesn't exist.
* If exists, overwrites model attributes values with values of changed fields
* in cached model.
* @return boolean true if model is stored in cache successfully.
*/
public function afterFind($event)
{
if (!$this->isEnabled)
return false;
$this->init();
if ($this->_cachedModel === false)
$this->createCachedModel();
else
{
foreach($this->_cachedModel['_systemAttrs']['_changedFields'] as $key => $value)
if ($key[0] != '_') // except hidden attributes, which begin with '_' symbol
{
$this->_attrs[$key] = $this->owner->$key = $this->_cachedModel[$key];
}
}
return $this->saveCachedModel();
}
/**
* This method is invoked after saving a record successfully.
* Creates model copy in cache after saving.
* @return boolean true if model is stored in cache successfully.
*/
public function afterSave($event)
{
if (!$this->isEnabled)
return false;
$this->init();
if ($this->isAnyCached())
{
$this->createCachedModel();
return $this->saveCachedModel();
}
}
/**
* This method is invoked after deleting a record.
* Deletes cached model from cache storage.
*/
public function afterDelete()
{
$this->deleteData($this->getCachedModelId());
}
/**
* This method is a "bridge" from AR instance to CachedModelBehavior
* instance. If {@link CachedModelBehavior::isEnabled} is false, or
* any of model attributes aren't cached, it returns CActiveRecord
* instance.
* You can get access to behavior methods, using construction
*
* $model->cm()->save();
*
* @return mixed CachedModelBehavior|CActiveRecord instance
* @api
*/
public function cm()
{
return ($this->isEnabled && $this->isAnyCached()) ? $this : $this->getOwner();
}
/**
* Saves the current record using cache.
* You can get access to behavior methods, using construction
*
* $model->cm()->save();
*
* @return boolean true if record was saved, false other way.
* @see ActiveRecord::save()
* @api
*/
public function save($runValidation = true, $attributes = null)
{
if(!$runValidation || $this->owner->validate($attributes))
return $this->owner->getIsNewRecord() ? $this->insert($attributes) : $this->update($attributes);
else
return false;
}
/**
* Saves a selected list of attributes using cache.
* Unlike {@link save()}, this method only saves the specified attributes
* of an existing model and does NOT call either {@link beforeSave} or {@link afterSave}.
* Please, see {@link CActiveRecord::saveAttributes()} for full description.
* @param array $attributes attributes to be updated. Each element represents an attribute name
* or an attribute value indexed by its name. If the latter, the record's
* attribute will be changed accordingly before saving.
* @return boolean whether the update is successful
* @throws CException if the record is new or any database error
* @see CActiveRecord::saveAttributes()
* @api
*/
public function saveAttributes($attributes)
{
if (!$this->owner->getIsNewRecord())
{
Yii::trace(get_class($this).'.saveAttributes()','CachedModelBehavior');
$this->init();
$attributes = $this->preprocessAttributes($attributes);
$values = $this->getValuesFromAttributes($attributes);
$this->updateCachedModel($values);
if ($this->isAllCached($attributes))
return $this->saveCachedModel();
// since we make DB query to save model attributes, we attach to
// original query all changed before attributes of cached model,
// so in fact we make synchronization of the whole cached model
// during saving attributes
$attributes = $this->_attachChangedAttributes($attributes);
$values = $this->getValuesFromAttributes($attributes);
if ($this->owner->saveAttributes($values))
{
$this->markAsSynchronized();
$result = true;
}
else
$result = false;
$this->saveCachedModel();
return $result;
}
else
throw new CDbException(Yii::t('yii','The active record cannot be updated because it is new.'));
}
/**
* Inserts a row, into the cache or directly into the DB table, based on this
* active record attributes. If {@link delayedInsert} is true and all attributes
* of model are cached, model is stored in cache, or, in other case, directly into
* main data storage. If model is stored in cache, it will be inserted into DB
* table during nearest synchronization. Please, see
* {@link CActiveRecord::insert()} for full description.
* @param array $attributes list of attributes that need to be saved. Defaults to null,
* meaning all attributes that are loaded from DB will be saved.
* @return boolean whether the attributes are valid and the record is inserted
* successfully.
* @throws CException if the record is not new
* @see CActiveRecord::insert()
* @api
*/
public function insert($attributes = null)
{
if(!$this->isAnyCached($attributes))
return $this->owner->insert($attributes);
if ($this->owner->getIsNewRecord())
{
$this->init();
if ($this->delayedInsert && $this->isAllCached($attributes))
return $this->addInsertModel(); // store model in cache
}
return $this->owner->insert($attributes);
}
/**
* Updates the model attributes in cache, if all changed attributes are cached.
* Or, in other case, updates DB table row represented by this active record.
* Please, see {@link CActiveRecord::update()} for full description.
* @param array $attributes list of attributes that need to be saved. Defaults to null,
* meaning all attributes that are loaded from DB will be saved in cache or DB.
* @return boolean whether the update is successful
* @see CActiveRecord::update()
* @api
*/
public function update($attributes = null)
{
if(!$this->isAnyCached($attributes))
return $this->owner->update($attributes);
if (!$this->owner->getIsNewRecord())
{
$this->init();
$values = $this->getValuesFromAttributes($attributes);
$this->updateCachedModel($values);
$result = $this->saveCachedModel();
if ($this->isAllCached($attributes))
return $result;
}
// since we make DB query to update table row, we attach to
// original query all changed before attributes of cached model,
// so in fact we make synchronization of the whole cached model
// during update
if ($attributes !== null)
$attributes = $this->_attachChangedAttributes($attributes);
if ($this->owner->update($attributes))
{
$this->markAsSynchronized();
$result = true;
}
else
$result = false;
$this->saveCachedModel();
return $result;
}
/**
* Saves one or several counter columns for the current AR object.
* Note that this method differs from {@link updateCounters} in that it only
* saves the current AR object.
* An example usage is as follows:
*
* $postRecord=Post::model()->findByPk($postID);
* $postRecord->cm()->saveCounters(array('view_count'=>1));
*
* Use negative values if you want to decrease the counters. Please, see
* {@link updateCounters} for full explanation of counters argument.
* @param array $counters the counters to be updated (column name=>value)
* @return boolean whether the saving is successful
* @throws CException if the record is new
* @see updateCounters()
* @see CActiveRecord::saveCounters()
* @api
*/
public function saveCounters($counters)
{
Yii::trace(get_class($this).'.saveCounters()','system.db.ar.CActiveRecord');
if(!$this->isAnyCached($counters))
return $this->owner->saveCounters($counters);
if (!$this->owner->getIsNewRecord())
{
$this->init();
$_c = $this->_normalizeCounters($counters);
Yii::trace('Counters: ' . print_r($counters, true) . '. Modified counters: ' . print_r($_c, true));
$this->updateCachedModel($_c);
$result = $this->saveCachedModel();
if ($this->isAllCached($counters))
return $result;
$counters = $this->getNotCachedAttributes($counters);
$this->_checkForNumericCounters($counters);
return $this->owner->saveCounters($counters);
}
else
throw new CDbException(Yii::t('yii','The active record cannot be updated because it is new.'));
}
/**
* Deletes all values from cache.
* Be careful of performing this operation if the cache is shared by multiple applications.
* @return boolean whether the flush operation was successful.
*/
public function flush()
{
return $this->_cache->flush();
}
/**
* Deletes the row corresponding to this active record.
* @return boolean whether the deletion is successful.
* @throws CException if the record is new
* @api
*/
public function delete()
{
return $this->owner->delete();
}
/**
* Updates one or several counter columns.
* Note, this updates all rows of data unless a condition or criteria is specified.
* Note, the counters are not checked for safety and validation is NOT performed.
* See {@link CActiveRecord::find()} for detailed explanation about $condition and
* $params. If row of data is represented in cached model, cached model is updated
* instead of table row.
* Counters may be defined in this way:
*
* $counters = array(
* 'userVisitsToday' => '+1',
* 'userValue' => '*2',
* 'userVisitsThisMonth' => '1',
* 'userVisitsThisYear' => 1,
* 'userNames' => '.' . $userName,
* 'userPoints' => '/2',
* 'lastVisitTime' => "=$time",
* );
*
* This example is equal to assignments:
*
* $model->userVisitsToday += 1;
* $model->userValue *= 2;
* $model->userVisitsThisMonth += 1;
* $model->userVisitsThisYear += 1;
* $model->userNames .= $userName;
* $model->userPoints /= 2;
* $model->lastVisitTime = $time;
*
* So, for cached attributes, counters may be defined with one of operators ('=',
* '+', '-', '*', '/', '%', '.') or without it. Counters may be numeric or not.
* Please, be careful with '/' operator for integer fields. Note, that '=' operator
* means direct assignment. Note, if attribute is not cached, its value should be as
* well as counter for {@link CActiveRecord::updateCounters()} method.
* Note, that if you define counters without operator, for example,
*
* $counters = array('userVisits' => '3', 'userPoints' => 2);
*
* system will interpret them as
*
* $counters = array('userVisits' => '+3', 'userPoints' => '+2');
*
* This option is developed to maintain compatibility with
* {@link CActiveRecord::updateCounters()} method.
* @param array $counters the counters to be updated (column name => value)
* @param mixed $condition query condition or criteria.
* @param array $params parameters to be bound to an SQL statement.
* @param array $autoload whether to create cached model for every being updated
* row and update cached model instead of table row. Do not set this option to
* true, if you suppose, too many rows will be updated. This way may lead to cache
* storage overflow.
* @return integer the number of rows being updated
* @throw CException if counter value is not numeric for not cached attributes or
* not cached models, when $autoload is false.
* @see saveCounters()
* @see updateByPk()
* @see updateAll()
* @see CActiveRecord::updateCounters()
* @api
*/
public function updateCounters($counters, $condition = '', $params = array(), $autoload = false)
{
Yii::trace(get_class($this) . '.updateCounters()', 'CachedModelBehavior');
return $this->internalUpdate($counters, array(), $condition, $params, $autoload, 'counters');
}
/**
* Updates records with the specified primary key(s).
* Note, the attributes are not checked for safety and validation is NOT performed.
* See {@link CActiveRecord::find()} for detailed explanation about $condition
* and $params. If row of data being updated is represented in cached model,
* cached model is updated instead of table row.
* See {@link updateCounters()} for detailed explanation about $attributes array
* (as well as $counters array).
* Note, that if you define attributes without operator, for example,
*
* $counters = array('userVisits' => '3', 'userPoints' => 2);
*
* system will interpret them (unlike {@link updateCounters()}) as
*
* $counters = array('userVisits' => '=3', 'userPoints' => '=2');
*
* This option is developed to maintain compatibility with
* {@link CActiveRecord::updateByPk()} method.
* @param mixed $pk primary key value(s). Use array for multiple primary keys.
* For composite key, each key value must be an array (column name => column value).
* @param array $attributes list of attributes (name => $value) to be updated
* @param mixed $condition query condition or criteria.
* @param array $params parameters to be bound to an SQL statement.
* @param array $autoload whether to create cached model for every being updated
* row and update cached model instead of table row. Do not set this option to
* true, if you suppose, too many rows will be updated. This way may lead to cache
* storage overflow.
* @return integer the number of rows being updated
* @see saveCounters()
* @see updateCounters()
* @see updateAll()
* @see CActiveRecord::updateByPk()
* @api
*/
public function updateByPk($pk, $attributes, $condition = '', $params = array(), $autoload = false)
{
Yii::trace(get_class($this) . '.updateByPk()', 'CachedModelBehavior');
return $this->internalUpdate($attributes, $pk, $condition, $params, $autoload, 'attributes');
}
/**
* Updates records with the specified condition.
* Note, the attributes are not checked for safety and validation is NOT performed.
* See {@link CActiveRecord::find()} for detailed explanation about $condition
* and $params. If row of data being updated is represented in cached model,
* cached model is updated instead of table row.
* See {@link updateCounters()} for detailed explanation about $attributes array
* (as well as $counters array).
* Note, that if you define attributes without operator, for example,
*
* $counters = array('userVisits' => '3', 'userPoints' => 2);
*
* system will interpret them (unlike {@link updateCounters()}) as
*
* $counters = array('userVisits' => '=3', 'userPoints' => '=2');
*
* This option is developed to maintain compatibility with
* {@link CActiveRecord::updateByPk()} method.
* @param array $attributes list of attributes (name => $value) to be updated
* @param mixed $condition query condition or criteria.
* @param array $params parameters to be bound to an SQL statement.
* @param array $autoload whether to create cached model for every being updated
* row and update cached model instead of table row. Do not set this option to
* true, if you suppose, too many rows will be updated. This way may lead to cache
* storage overflow.
* @return integer the number of rows being updated
* @see saveCounters()
* @see updateCounters()
* @see updateByPk()
* @see CActiveRecord::updateAll()
* @api
*/
public function updateAll($attributes, $condition = '', $params = array(), $autoload = false)
{
Yii::trace(get_class($this) . '.updateAll()', 'CachedModelBehavior');
return $this->internalUpdate($attributes, array(), $condition, $params, $autoload, 'attributes');
}
/**
* Deletes rows with the specified primary key. If row being deleted is
* represented in cached model, cached model will be deleted immediately after
* original row is deleted.
* See {@link find()} for detailed explanation about $condition and $params.
* @param mixed $pk primary key value(s). Use array for multiple primary keys. For composite key, each key value must be an array (column name=>column value).
* @param mixed $condition query condition or criteria.
* @param array $params parameters to be bound to an SQL statement.
* @return integer the number of rows deleted
* @see CActiveRecord::deleteByPk()
* @see deleteAllByAttributes()
* @see deleteAll()
* @api
*/
public function deleteByPk($pk, $condition = '', $params = array())
{
Yii::trace(get_class($this) . '.deleteByPk()', 'CachedModelBehavior');
return $this->internalDelete($pk, array(), $condition, $params);
}
/**
* Deletes rows with the specified condition. If row being deleted is
* represented in cached model, cached model will be deleted immediately after
* original row is deleted.
* See {@link find()} for detailed explanation about $condition and $params.
* @param mixed $condition query condition or criteria.
* @param array $params parameters to be bound to an SQL statement.
* @return integer the number of rows deleted
* @see CActiveRecord::deleteAll()
* @see deleteAllByAttributes()
* @see deleteByPk()
* @api
*/
public function deleteAll($condition = '', $params = array())
{
Yii::trace(get_class($this) . '.deleteAll()', 'CachedModelBehavior');
return $this->internalDelete(array(), array(), $condition, $params);
}
/**
* Deletes rows which match the specified attribute values. If row being deleted is
* represented in cached model, cached model will be deleted immediately after
* original row is deleted.
* See {@link find()} for detailed explanation about $condition and $params.
* @param array $attributes list of attribute values (indexed by attribute names)
* that the active records should match.
* An attribute value can be an array which will be used to generate an IN condition.
* @param mixed $condition query condition or criteria.
* @param array $params parameters to be bound to an SQL statement.
* @return integer number of rows affected by the execution.
* @see CActiveRecord::deleteAllByAttributes()
* @see deleteAll()
* @see deleteByPk()
* @api
*/
public function deleteAllByAttributes($attributes, $condition = '', $params = array())
{
Yii::trace(get_class($this) . '.deleteAllByAttributes()', 'CachedModelBehavior');
return $this->internalDelete(array(), $attributes, $condition, $params);
}
/**
* Initializes some internal properties. Call in the beginning of each user
* api method.
*/
public function init()
{
$this->_attrs = $this->owner->attributes;
if (!$this->owner->getIsNewRecord())
$this->_cachedModel = $this->loadData($this->getCachedModelId());
}
/**
* Checks whether all attributes are cached.
* @param array $attrubutes list of attributes. Defaults to null what means
* 'all attributes of the model'.
* @return boolean true if all listed attributes are cached, false otherwise.
* @see isAnyCached
*/
public function isAllCached($attributes = null)
{
if (empty($this->fields) && empty($this->excludedFields))
return true;
$attributes = $this->preprocessAttributes($attributes);
foreach($attributes as $field)
{
if (empty($this->fields))
{
if(in_array($field, $this->excludedFields))
return false;
} else {
if (!in_array($field, $this->fields))
return false;
}
}
return true;
}
/**
* Checks whether at least one attribute is cached.
* @param array $attrubutes list of attributes. Defaults to null what means
* 'all attributes of the model'.
* @return boolean true if at least one among listed attributes is cached,
* false otherwise.
* @see isAllCached
*/
public function isAnyCached($attributes = null)
{
if (empty($this->fields) && empty($this->excludedFields))
return true;
$attributes = $this->preprocessAttributes($attributes);
foreach($attributes as $field)
{
if (empty($this->fields))
{
if(!in_array($field, $this->excludedFields))
return true;
} else {
if (in_array($field, $this->fields))
return true;
}
}
return false;
}
/**
* Checks for long fields. We assume that BLOB, TEXT and their derivative field
* types are long. And also VARCHAR, CHAR, BINARY, VARBINARY field types with
* length more than {@link FIELD_MAX_LENGTH}. The aim is not cache long
* fields to avoid wasting cache resources.
* @return array list of long fields.
* @see showLongFieldsWarnings()
*/
public function getLongFields()
{
$result = array();
foreach ($this->_cols as $key => $field)
{
$fieldType = $field->dbType;
$size = $field->size;
$isBlob = stripos($fieldType, 'blob') !== false;
$isText = stripos($fieldType, 'text') !== false;
$isChar = stripos($fieldType, 'char') !== false;
$isBinary = stripos($fieldType, 'binary') !== false;
if ($isBlob || $isText ||
(($isChar || $isBinary) && $size > self::FIELD_MAX_LENGTH))
$result[$key] = $fieldType;
}
return $result;
}
/**
* Creates cached model.
* @param array $attrubutes list of attributes. Defaults to null what means
* 'all attributes of the model'.
* @param boolean $forceOverwrite whether to overwrite cached model attributes,
* if cached model already exists in cache.
*/
public function createCachedModel($attributes = null, $forceOverwrite = true)
{
$attributes = $this->preprocessAttributes($attributes);
$cmId = $this->getCachedModelId();
$oldCachedModel = $this->loadData($cmId);
$cachedModel = array();
foreach($attributes as $attr)
{
if ((empty($this->fields) || in_array($attr, $this->fields))
&& !in_array($attr, $this->excludedFields))
{
$cachedModel[$attr] = isset($this->_attrs[$attr]) ? $this->_attrs[$attr] : null;
}
}
if (!empty($oldCachedModel))
{
if ($forceOverwrite)
{
$cachedModel = array_merge($oldCachedModel, $cachedModel);
$cachedModel['_systemAttrs'] = array_merge($cachedModel['_systemAttrs'], array(
'_isChanged' => false,
'_lastSyncTime' => time(),
'_lastChangeTime' => time(),
'_changedFields' => array(),
));
}
else
$cachedModel = array_merge($cachedModel, $oldCachedModel);
}
else
{
$cachedModel['_systemAttrs'] = $this->_systemAttrs;
$cachedModel['_systemAttrs']['_id'] = empty($cachedModel['_systemAttrs']['_id']) ?
$this->getModelId($cachedModel) : $cachedModel['_systemAttrs']['_id'];
}
$this->_cachedModel = $cachedModel;
}
/**
* Get id in cache for list of models ids, changed after last synchronization.
* @return id of changed models ids list
* @see syncChangedModels()
*/
public function getChangedModelsId()
{
if ($this->_changedModelsId !== null)
return $this->_changedModelsId;
$this->_changedModelsId = self::CACHE_KEY_PREFIX . get_class($this->owner) . '._changedModels';
return $this->_changedModelsId;
}
/**
* Get id in cache for list of models, inserted after last synchronization.
* @return id of inserted models
* @see syncInsertModels()
*/
public function getInsertModelsId()
{
if ($this->_insertModelsId !== null)
return $this->_insertModelsId;
$this->_insertModelsId = self::CACHE_KEY_PREFIX . get_class($this->owner) . '._insertModels';
return $this->_insertModelsId;
}
/**
* Get id in cache for cached model.
* @param array $cachedModel if not current cached model id needed.
* If null, returns id in cache for current
* @return id of cached model
*/
public function getCachedModelId($cachedModel = null)
{
if ($cachedModel === null)
{
if ($this->_cachedModelId !== null)
return $this->_cachedModelId;
$this->_cachedModelId = self::CACHE_KEY_PREFIX . get_class($this->owner) . '.' . $this->getModelId();
return $this->_cachedModelId;
} else {
if (is_array($cachedModel))
return self::CACHE_KEY_PREFIX . get_class($this->owner) . '.' . $this->getModelId($cachedModel);
else
return self::CACHE_KEY_PREFIX . get_class($this->owner) . '.' . $cachedModel;
}
}
/**
* Get id in cache where stores last synchronization time.
* @return id of last synchronization time.
*/
public function getLastSyncId()
{
if ($this->_lastSyncId !== null)
return $this->_lastSyncId;
$this->_lastSyncId = self::CACHE_KEY_PREFIX . get_class($this->owner) . '._lastSync';
return $this->_lastSyncId;
}
/**
* Get id for model.
* @param array $cachedModel actual cached model. If null, id for current
* model will be returned.
* @param string $type type of returned result. May be 'array' or 'string'.
* @return id of model.
*/
public function getModelId($cachedModel = null, $type = 'string')
{
if ($cachedModel !== null
&& (empty($cachedModel) || !is_array($cachedModel)))
return false;
if ($this->_id !== null && $cachedModel === null
// if id is of appropriate type, when we have composite primary key...
&& ((is_array($this->_id) && is_array($this->_keyField) && $type == 'array')
|| (is_string($this->_id) && is_array($this->_keyField) && $type == 'string')
// or when we have simple primary key
|| !is_array($this->_keyField)))
return $this->_id;
if (is_string($this->_keyField))
{
if ($cachedModel === null)
return $this->_id = $this->_attrs[$this->_keyField];
else
return $cachedModel[$this->_keyField];
}
else if (is_array($this->_keyField))
{
$values = array();
foreach($this->_keyField as $name)
if ($cachedModel === null)
// $this->_cachedModel[$name] may be not cached, so $this->_attrs[$name] is correct
$values[$name] = $this->_attrs[$name];
else
$values[$name] = $cachedModel[$name];
if ($type == 'string')
$values = implode('.', $values);
if ($cachedModel === null)
$this->_id = $values;
return $values;
}
else
return $this->_id = null;
}
/**
* Generates warning in log, when long fields are cached.
* @return boolean true, if warning was generated, false otherwise.
* @see getLongFields()
*/
public function showLongFieldsWarnings()
{
$longFields = $this->getLongFields();
$longFieldsNames = array_keys($longFields);
$warnings = empty($this->fields) ?
array_diff($longFieldsNames, $this->excludedFields) :
array_intersect($longFieldsNames, $this->fields);
if (!empty($warnings))
{
$fields = '';
foreach ($warnings as $name)
$fields .= '"' . $name . ' (' . $longFields[$name] . ')", ';
$fields = trim($fields, ', ') . '.';
Yii::log(Yii::t('cachedModel', 'These attributes from class "{class}" may waste cache resources: {fields}', array(
'{fields}' => $fields,
'{class}' => get_class($this->owner),
)), 'warning');
return true;
}
return false;
}
/**
* Updates cached model.
* @param array $values values for update. Please, see {@link updateCounters()}
* for full explanation.
* @param array $cachedModel actual cached model. If null, current cached model
* is updated.
* @return mixed updated cached model if $cachedModel is not null. If
* $cachedModel if not null, returns true when current cached model update was
* successful, false otherwise.
* @throws CException if value has invalid operator
*/
public function updateCachedModel($values = null, $cachedModel = null)
{
// $cmId = $this->getCachedModelId($cachedModel);
if (empty($this->_cachedModel) && empty($cachedModel))
return $this->createCachedModel();
if ($values === null)
$values = $this->_attrs;
if (!is_array($values) || empty($values))
return empty($cachedModel) ? false : $cachedModel;
$changes = $this->getAttributesChanges($values, $cachedModel);
$nullFields = array();
Yii::trace('Changes: ' . print_r($changes, true));
if (empty($changes))
return empty($cachedModel) ? false : $cachedModel;
// to avoid values of primary keys update
$this->_checkForPkUpdate($changes);
foreach ($changes as $elemKey => $elem)
{
$k = $elem['field'];
$v = $elem['value'];
if (empty($cachedModel))
$cm = &$this->_cachedModel;
else
$cm = &$cachedModel;
if (isset($cm[$k]) || array_key_exists($k, $cm))
{
// if key exists, but attribute value is NULL, only operator '=' is allowed
if ($elem['op'] == '=' || isset($cm[$k]))
switch ($elem['op'])
{
case '=' : $cm[$k] = $v; break;
case '-' : $cm[$k] = (float) $cm[$k] - (float) $v; break;
case '+' : $cm[$k] = (float) $cm[$k] + (float) $v; break;
case '/' : $cm[$k] = (float) $cm[$k] / (float) $v; break;
case '*' : $cm[$k] = (float) $cm[$k] * (float) $v; break;
case '%' : $cm[$k] = (float) $cm[$k] % (float) $v; break;
case '.' : $cm[$k] .= $v; break;
default :
throw new CException(Yii::t('cachedModel', 'Unknown operator "{operator}" in model "{class}"', array(
'{operator}' => $elem['op'],
'{class}' => get_class($this->owner)
)));
}
else
{
Yii::log(Yii::t('cachedModel', 'You tried to use not defined value for field "{field}", class "{class}".', array(
'{field}' => $k,
'{class}' => get_class($this->owner),
)), 'warning');
unset($changes[$elemKey]);
}
if (empty($cachedModel))
$this->owner->$k = $this->_attrs[$k] = $cm[$k];
}
else
throw new CException(Yii::t('cachedModel', 'Wrong field "{field}" for model "{class}"', array(
'{field}' => $elem['field'],
'{class}' => get_class($this->owner),
)));
}
$cachedModel = $this->markAsNotSynchronized($changes, $cachedModel);
return empty($cachedModel) ? true : $cachedModel;
}
/**
* Adds changed model id to list of changed models ids.
* @return boolean true on success, false otherwise.
*/
public function addChangedModel($id)
{
$changedModelsId = $this->getChangedModelsId();
$changedModels = $this->loadData($changedModelsId);
if ($changedModels !== false)
$changedModels[$id] = isset($changedModels[$id]) ? ++$changedModels[$id] : 1;
else
$changedModels = array(
$id => 1,
);
return $this->saveData($changedModelsId, $changedModels, $this->cacheDuration * 3);
}
/**
* Removes model id from list of changed models ids.
* @return boolean true on success, false otherwise.
*/
public function removeChangedModel($id)
{
$changedModelsId = $this->getChangedModelsId();
$changedModels = $this->loadData($changedModelsId);
if ($changedModels !== false)
{
unset($changedModels[$id]);
return $this->saveData($changedModelsId, $changedModels, $this->cacheDuration * 3);
}
}
/**
* Adds insert model to list of insert models.
* @return boolean true on success, false otherwise.
*/
public function addInsertModel()
{
$insertModelsId = $this->getInsertModelsId();
$insertModels = $this->loadData($insertModelsId);
$insertModels[] = $this->_attrs;
return $this->saveData($insertModelsId, $insertModels, $this->cacheDuration * 3);
}
/**
* Makes several checks and generates array of changes according to $values.
* @param array $values changes data in brief mode
* @param array $cachedModel actual cached model. If null, current cached model
* will be used.
* @return array array of changes.
*/
public function getAttributesChanges($values, $cachedModel = null)
{
$changes = array();
foreach($values as $key => $value)
{
if ((!empty($cachedModel) && !array_key_exists($key, $cachedModel))
|| !array_key_exists($key, $this->_cachedModel))
continue;
if ($value === '')
$value = '=';
if (!in_array($value[0], array('=', '+', '-', '*', '/', '%', '.')))
$value = '=' . $value;
preg_match('#([=\-+*/%.])(.*)#', $value, $value);
if (empty($value))
throw new CException ("Wrong format in field \"$key\".");
$element = array(
'field' => $key,
'op' => $value[1],
'value' => $value[2],
);
$attrValue = empty($cachedModel) ?
$this->_cachedModel[$element['field']] : $cachedModel[$element['field']];
if (!(in_array($element['op'], array('-', '+')) &&
$element['value'] == 0) && // '+0', '-0' cases
!(in_array($element['op'], array('*', '/')) &&
$element['value'] == 1) && // '*1', '/1' cases
!($element['op'] == '=' &&
$element['value'] == $attrValue)) // the same value assignment
{
$changes[] = $element;
}
}
return $changes;
}
/**
* Marks cached model as NOT synchronized (changed after last synchronization).
* @param array $changes changes that were implemented to cached model
* @param array $cachedModel actual cached model. If null, current cached model
* will be used.
* @return array changed cached model, if $cachedModel is not null
*/
public function markAsNotSynchronized($changes, $cachedModel = null)
{
$fields = array();
foreach ($changes as $element)
if (is_array($element))
$fields[] = $element['field'];
else
$fields[] = $element;
if (empty($cachedModel))
$_cf = &$this->_cachedModel['_systemAttrs']['_changedFields'];
else
$_cf = &$cachedModel['_systemAttrs']['_changedFields'];
// increase counter, how many times current field was changed.
// may be useful for future development.
foreach($fields as $field)
$_cf[$field] = isset($_cf[$field]) ? ++$_cf[$field] : 1;
if (empty($cachedModel))
{
$this->_cachedModel['_systemAttrs']['_lastChangeTime'] = time();
$this->_cachedModel['_systemAttrs']['_isChanged'] = true;
$this->addChangedModel($this->getModelId());
} else {
$cachedModel['_systemAttrs']['_lastChangeTime'] = time();
$cachedModel['_systemAttrs']['_isChanged'] = true;
$this->addChangedModel($this->getModelId($cachedModel));
return $cachedModel;
}
}
/**
* Marks cached model as synchronized (NOT changed after last synchronization).
* @param array $cachedModel actual cached model. If null, current cached model
* will be used.
* @return array changed cached model, if $cachedModel is not null
*/
public function markAsSynchronized($cachedModel = null)
{
if (empty($cachedModel))
{
$_sa = &$this->_cachedModel['_systemAttrs'];
$this->removeChangedModel($this->getModelId());
} else {
$_sa = &$cachedModel['_systemAttrs'];
$this->removeChangedModel($this->getModelId($cachedModel));
}
$_sa['_changedFields'] = array();
$_sa['_lastSyncTime'] = time();
$_sa['_isChanged'] = false;
return $cachedModel;
}
/**
* Saves current cached model. Run synchronization process then.
* @return boolean true if current cached model was saved successfully,
* false otherwise.
* @see syncModels()
*/
public function saveCachedModel()
{
$result = $this->saveData($this->getCachedModelId(), $this->_cachedModel);
$this->syncModels();
return $result;
}
/**
* Gets values from attributes.
* @return array of values 'array(column name => value)'
*/
public function getValuesFromAttributes($attributes)
{
$values = array();
foreach($attributes as $name => $value)
{
if(is_integer($name))
$values[$value] = $this->_attrs[$value];
else
$values[$name] = $this->_attrs[$name] = $value;
}
return $values;
}
/**
* Starts synchronization process, if last synchronization was started more than
* {@link syncInterval} seconds before.
* @return integer count of synchronized models.
*/
public function syncModels()
{
if ($this->_lastSync === null)
$this->_lastSync = $this->loadData($this->getLastSyncId());
$time = time();
if ($this->_lastSync === false)
{
$this->saveData($this->getLastSyncId(), $time, $this->cacheDuration * 3);
$this->_lastSync = $time;
return false;
}
if ($time < $this->_lastSync + $this->syncInterval)
return false;
$this->saveData($this->getLastSyncId(), $time, $this->cacheDuration * 3);
$this->_lastSync = $time;
$result = $this->syncInsertModels();
$result += $this->syncChangedModels();
return $result;
}
/**
* Synchronizes insert models
* @return integer count of synchronized models.
*/
public function syncInsertModels()
{
$imId = $this->getInsertModelsId();
$insertModels = $this->loadData($imId);
$className = strtolower(get_class($this->owner));
$result = 0;
Yii::trace('Synchronization. Inserts: ' . print_r($insertModels, true));
if (!empty($insertModels))
{
// is it mysql or pgsql drivers, which support multiple insert?
if (in_array(Yii::app()->db->getDriverName(), array('mysql', 'pgsql')))
{
$i = 0; $total = count($insertModels);
do
{
// split all inserts to packets to avoid max_allowed_packed overhead
$values = $fields = $_fields = ''; $realValues = array();
do
{
$_values = ''; $model = $insertModels[$i];
foreach ($model as $key => $value)
{
if (empty($fields))
$_fields .= '`' . $key . '`, ';
$valueKey = ":r{$i}_{$key}";
$_values .= "$valueKey, ";
$realValues[$valueKey] = $value;
}
$fields = '(' . trim($_fields, ', ') . ')';
$values .= '(' . trim($_values, ', ') . '), ';
} while (++$i % self::INSERTS_PER_COMMAND > 0 && $i < $total);
$values = trim($values, ', ');
$insertSql = 'insert into {{' . $className . '}} ' . $fields . ' values ' . $values . ';';
Yii::trace("SQL: $insertSql. Values: ". print_r($realValues, true));
$result += Yii::app()->db->createCommand($insertSql)->execute($realValues);
} while ($i < $total);
} else {
// Note: Oracle, MS SQL, SQLite drivers support multiple inserts as well,
// but they all have different SQL syntax. You can improve this class
// with handle this piece of work, if it is necessary. But now we
// add one row per insert command for these DB drivers.
foreach ($insertModels as $model)
{
$builder=$this->owner->getCommandBuilder();
$table=$this->owner->getMetaData()->tableSchema;
$command=$builder->createInsertCommand($table,$model);
$result += $command->execute();
}
}
if ($result > 0)
$this->saveData($imId, array(), $this->cacheDuration * 3);
}
return $result;
}
/**
* Synchronizes changed models
* @return integer count of synchronized models.
*/
public function syncChangedModels()
{
$cmId = $this->getChangedModelsId();
$changedModels = $this->loadData($cmId);
$result = 0;
Yii::trace('Synchronization. Changed models: ' . print_r($changedModels, true));
if (!empty($changedModels))
foreach ($changedModels as $modelId => $changedTimes)
$result += (int) $this->syncChangedModel($modelId);
if ($result > 0)
$this->saveData($cmId, array(), $this->cacheDuration * 3);
return $result;
}
/**
* Synchronizes changed model
* @param string $id id of synchronized model
* @return boolean true if success, false otherwise
*/
public function syncChangedModel($id)
{
$cachedModelId = $this->getCachedModelId($id);
$cachedModel = $this->loadData($cachedModelId);
$_sa = &$cachedModel['_systemAttrs'];
$_cf = &$cachedModel['_systemAttrs']['_changedFields'];
if (empty($_cf))
$this->removeChangedModel($id);
else
{
$values = array();
foreach($_cf as $key => $value)
$values[$key] = $cachedModel[$key];
$_pk = $this->getModelId($cachedModel, 'array');
if (!empty($values))
{
$this->owner->updateByPk($_pk, $values);
$cachedModel = $this->markAsSynchronized($cachedModel);
$this->saveData($cachedModelId, $cachedModel);
return true;
}
}
return false;
}
/**
* Gets not cached attributes from $attributes
* @param array $attributes attributes to be checked 'array(column name => value)',
* associative array
* @return array of not cached attributes and values
*/
public function getNotCachedAttributes($attributes)
{
$this->_showNotCachedAttributesWarning($attributes);
// look for existed attributes...
$_c = empty($this->fields) ?
array_diff(array_keys($attributes), $this->excludedFields) :
array_intersect($this->fields, array_keys($attributes));
// ... and remove them from array for DB update
foreach($_c as $field)
unset($attributes[$field]);
return $attributes;
}
/**
* @internal
* Updates cached models and table rows.
* @see updateCounters
* @see updateByPk
* @see updateAll
*/
protected function internalUpdate(
$attributes,
$pks = array(),
$condition = '',
$params = array(),
$autoload = false,
$attrType = 'attributes' // can be 'counters' when update counters
// and 'attributes' when update attributes
)
{
$builder = $this->owner->getCommandBuilder();
$table = $this->owner->getTableSchema();
if (empty($pks))
$criteria = $builder->createCriteria($condition,$params);
else
{
$pks = $this->_extractPks($pks);
$criteria = $builder->createPkCriteria($table, $pks, $condition, $params, 't.');
}
$criteria->select = $this->_keyField;
$command = $builder->createFindCommand($table, $criteria);
$pks = $this->_convertPksToStrings($command->queryAll());
// var_export($pks); die();
// do actual cached models update here
$updatedPks = $this->getUpdatedCachedPks($pks, $attributes, $autoload, $attrType);
$result = count($updatedPks);
$allModelsUpdated = count($pks) == count($updatedPks);
if ($allModelsUpdated && $this->isAllCached($attributes))
return $result;
Yii::trace('All pks: ' . print_r($pks, true) . '; updatedpks : ' . print_r($updatedPks, true));
if (!$allModelsUpdated)
{
$notUpdatedPks = array_diff_key($pks, $updatedPks);
$notUpdatedPks = $this->_extractPks($notUpdatedPks);
$criteria = $builder->createPkCriteria(
$this->_table, array_values($notUpdatedPks), $condition, $params
);
if ($attrType == 'counters')
{
$this->_checkForNumericCounters($attributes);
$command = $builder->createUpdateCounterCommand(
$this->owner->getTableSchema(), $attributes, $criteria
);
}
else
$command = $builder->createUpdateCommand(
$this->owner->getTableSchema(), $attributes, $criteria
);
Yii::trace('Not all models updated.');
$result += $command->execute();
}
if (!$this->isAllCached($attributes))
{
$notCachedAttributes = $this->getNotCachedAttributes($attributes);
$updatedPks = $this->_extractPks($updatedPks);
$criteria = $builder->createPkCriteria(
$this->_table, array_values($updatedPks), $condition, $params
);
if ($attrType == 'counters')
{
$this->_checkForNumericCounters($attributes);
$command = $builder->createUpdateCounterCommand(
$this->owner->getTableSchema(), $notCachedAttributes, $criteria
);
}
else
$command = $builder->createUpdateCommand(
$this->owner->getTableSchema(), $notCachedAttributes, $criteria
);
Yii::trace('Not all attributes updated.');
$command->execute();
}
return $result;
}
/**
* @internal
* Deletes cached models and table rows.
* @see deleteAll
* @see deleteByPk
* @see deleteAllByAttributes
*/
protected function internalDelete($pk, $attributes, $condition, $params)
{
$builder = $this->owner->getCommandBuilder();
$table = $this->owner->getTableSchema();
$pk = $this->_extractPks($pk);
if (!empty($pk))
$criteria = $builder->createPkCriteria($table, $pk, $condition, $params, 't.');
else if (!empty($attributes))
$criteria = $builder->createColumnCriteria($table, $attributes, $condition, $params, 't.');
else
$criteria = $builder->createCriteria($condition,$params);
$criteria->select = $this->_keyField;
$queryCommand = $builder->createFindCommand($table, $criteria);
$pks = $this->_convertPksToStrings($queryCommand->queryAll());
if (!empty($pk))
$criteria = $builder->createPkCriteria($table, $pk, $condition, $params);
else if (!empty($attributes))
$criteria = $builder->createColumnCriteria($table, $attributes, $condition, $params);
$deleteCommand = $builder->createDeleteCommand($table, $criteria);
$result = $deleteCommand->execute();
if (empty($result))
return false;
foreach($pks as $id => $value)
{
$cmId = $this->getCachedModelId($id);
$cm = $this->loadData($cmId);
if (!empty($cm))
$this->deleteData($cmId);
}
}
/**
* @internal
* Fit $fields argument to appropriate form and checks whether all fields exist
* in DB table.
* @param mixed string|array $fields
* @return array of checked fields
* @throws CException when some field is invalid
*/
protected function processFields($fields)
{
if (empty($fields))
return array();
if (is_string($fields))
$fields = preg_split('#\s*,\s*#', $fields, 0, PREG_SPLIT_NO_EMPTY);
if (!is_array($fields))
$fields = array($fields);
foreach($fields as $field)
if (!in_array($field, array_keys($this->_cols)))
throw new CException(Yii::t('cachedModel', 'Field "{field}" does not exist in table "{table}"', array(
'{field}' => $field,
'{table}' => $this->_table,
)));
return $fields;
}
/**
* @internal
* Fit $attributes argument to appropriate form and checks whether all attributes
* exist in DB table.
* @param mixed string|array $fields
* @return array of checked attributes
* @throws CException when some attribute is invalid
*/
protected function preprocessAttributes($attributes)
{
if ($attributes === null)
$attributes = array_keys($this->_attrs);
if (is_string($attributes))
$attributes = preg_split('#\s*,\s*#', $attributes, 0, PREG_SPLIT_NO_EMPTY);
if (is_string($attributes))
$attributes = array($attributes);
// assure that $attributes is non associative array
$attributes = array_keys($this->getValuesFromAttributes($attributes));
foreach($attributes as $field)
{
if (!in_array($field, array_keys($this->_cols)))
throw new CException(Yii::t(
'cachedModel',
'Field "{field}" is not valid property for class "{class}"',
array(
'{field}' => $field,
'{class}' => get_class($this->owner),
)));
}
return $attributes;
}
/**
* @internal
* Loads data from cache.
* @param string $id id of data in cache storage
* @return mixed loaded data
* @see ICache::get()
*/
protected function loadData($id)
{
$data = $this->_cache->get($id);
Yii::trace(Yii::t('cachedModel', 'Retrieved from cache id "{id}" with data "{data}".', array(
'{id}' => $id,
'{data}' => print_r($data, true),
)));
return $data;
}
/**
* @internal
* Saves data to cache.
* @param string $id id of data in cache storage
* @param mixed $data data to be stored in cache
* @param integer $duration how long data will be valid in cache. If null,
* {@link cacheDuration} property will be used.
* @return boolean true if saving was successfull, false otherwise.
* @see ICache::set()
*/
protected function saveData($id, $data, $duration = null)
{
if ($duration === null)
$duration = $this->cacheDuration;
Yii::trace(Yii::t('cachedModel', 'Store to cache id "{id}" with data "{data}", duration is "{duration}".', array(
'{id}' => $id,
'{data}' => print_r($data, true),
'{duration}' => $duration,
)));
return $this->_cache->set($id, $data, $duration);
}
/**
* @internal
* Delete data from cache.
* @param string $id id of data in cache storage
* @see ICache::delete()
*/
protected function deleteData($id)
{
if ($this->_cache->delete($id))
Yii::trace(Yii::t('cachedModel', 'Deleted key "{key}" from cache', array('{key}' => $id)));
}
/**
* @internal
* Updates cached models according to $attributes column names and values.
* @param string $pks of models
* @param array $attributes attributes to update models array(column name => value)
* @param boolean $autoload whether to create cached model, if it not exists in cache
* @param string $type type of attributes: are they counters or attributes
*/
protected function getUpdatedCachedPks($pks, $attributes, $autoload, $type = 'counters')
{
$result = array();
switch ($type)
{
case 'counters' : $attributes = $this->_normalizeCounters($attributes); break;
case 'attributes' : break;
default : throw new CException(Yii::t('cachedModel', 'Wrong type parameter "{type}"',
array('{type}' => $type)));
}
if (!is_array($pks))
$pks = array($pks);
foreach($pks as $key => $value)
{
$cmId = $this->getCachedModelId($key);
// echo "$key " . serialize($value) . " $cmId;";
$cm = $this->loadData($cmId);
if ($autoload === true && empty($cm))
{
if (is_string($this->_keyField))
$this->owner->findByPk($key);
else
$this->owner->findByPk($value);
$cm = $this->loadData($this->getCachedModelId($key));
}
if (!empty($cm))
{
$cm = $this->updateCachedModel($attributes, $cm);
$this->saveData($cmId, $cm);
$result[$key] = $value;
}
}
$this->init();
return $result;
}
// converts primary key to string and returns associative array
private function _convertPksToStrings($pks)
{
$result = array();
if (is_array($pks))
{
foreach($pks as $pk)
if (is_array($pk))
$result[implode('.', $pk)] = $pk;
return $result;
}
else
return $pks;
}
/**
* @internal
* Attach attributes, which were changed in cached model earlier,
* during model updating or saving some attributes
*/
private function _attachChangedAttributes($attributes)
{
$_cf = $this->_cachedModel['_systemAttrs']['_changedFields'];
if (empty($attributes))
return array();
if (isset($attributes[0])) // is it usual array?
return array_values(array_unique(array_merge($attributes, array_keys($_cf))));
else
{
foreach ($_cf as $field => $value)
if (!isset($attributes[$field]))
$attributes[$field] = $this->_attrs[$field];
}
return $attributes;
}
// used for counters only
private function _normalizeCounters($counters)
{
$result = array();
foreach($counters as $key => $value)
{
if (is_numeric($value))
{
if ($value >= 0)
$result[$key] = '+' . (float) $value;
else
$result[$key] = '-' . (float) -$value;
}
else
$result[$key] = $value;
}
return $result;
}
/**
* @internal
* Checks for update values of primary keys. If primary key will be updated,
* generates exception. Because of primary key update is dangerous procedure.
* @throws CException on primary key update
*/
private function _checkForPkUpdate($changes)
{
$pkFields = $this->_keyField;
if (!is_array($pkFields))
$pkFields = array($pkFields);
Yii::trace('Check for pk update');
$warnings = array();
foreach($changes as $change)
if (in_array($change['field'], $pkFields))
$warnings[] = $change['field'];
if (!empty($warnings))
throw new CException(Yii::t('cachedModel', 'You can\'t change fields "{fields}" in primary key.',
array('{fields}' => implode(', ', $warnings))));
}
// generates log warning if model has not cached attributes
private function _showNotCachedAttributesWarning($counters)
{
$warnings = empty($this->fields) ?
array_intersect(array_keys($counters), $this->excludedFields) :
array_diff(array_keys($counters), $this->fields);
Yii::log(Yii::t('cachedModel', 'You should store all counters "{fields}" in cache for class "{class}" to improve performance and update capabilities.', array(
'{fields}' => implode(', ', $warnings),
'{class}' => get_class($this->owner),
)), 'warning');
return $warnings;
}
private function _checkForNumericCounters($counters)
{
foreach($counters as $key => $value)
if (!is_numeric($value))
throw new CException(Yii::t('cachedModel',
'You should set number in index "{index}" for update',
array('{index}' => $key)));
}
private function _extractPks($pks)
{
if (is_string($this->_keyField))
{
$result = array();
foreach ($pks as $pk)
$result[] = $pk[$this->_keyField];
return $result;
}
else
return $pks;
}
}