123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644 |
- <?php
- /**
- * Allows access to redis records using the active record pattern.
- * <pre>
- * $record = ARedisRecord::model()->findByPk(1); // loads a record with a unique id of 1
- * $record->name = "a test name"; // sets the name attribute on the record
- * $record->save(); // saves the record to redis
- * $record->delete(); // deletes the record from redis
- * </pre>
- * @author Charles Pick / PeoplePerHour.com
- * @package packages.redis
- */
- abstract class ARedisRecord extends CFormModel {
- /**
- * The redis connection
- * @var ARedisConnection
- */
- public static $redis;
- /**
- * The record attributes.
- * @var CAttributeCollection
- */
- protected $_attributes;
- /**
- * The connection to redis
- * @var ARedisConnection
- */
- protected $_connection;
- /**
- * The redis set that represents the list of models of this type
- * @var ARedisIterableSet
- */
- protected $_redisSet;
- /**
- * The redis hash that contains the values for this record
- * @var ARedisIterableHash
- */
- protected $_redisHash;
- /**
- * The old primary key value
- * @var mixed
- */
- private $_pk;
- /**
- * Whether this is a new record or not
- * @var boolean
- */
- private $_new = true;
- /**
- * An array of static model instances, clas name => model
- * @var array
- */
- private static $_models=array();
- /**
- * Constructor.
- * @param string $scenario the scenario name
- * See {@link CModel::scenario} on how scenario is used by models.
- * @see getScenario
- */
- public function __construct($scenario = "insert")
- {
- if ($scenario === null) {
- return;
- }
- $this->init();
- $this->attachBehaviors($this->behaviors());
- $this->afterConstruct();
- }
- /**
- * Returns the static model of the specified redis record class.
- * The model returned is a static instance of the redis record class.
- * It is provided for invoking class-level methods (something similar to static class methods.)
- *
- * EVERY derived redis record class must override this method as follows,
- * <pre>
- * public static function model($className=__CLASS__)
- * {
- * return parent::model($className);
- * }
- * </pre>
- *
- * @param string $className redis record class name.
- * @return ARedisRecord redis record model instance.
- */
- public static function model($className=__CLASS__)
- {
- if(isset(self::$_models[$className]))
- return self::$_models[$className];
- else
- {
- $model=self::$_models[$className]=new $className(null);
- $model->attachBehaviors($model->behaviors());
- return $model;
- }
- }
- /**
- * Returns the redis connection used by redis record.
- * By default, the "redis" application component is used as the redis connection.
- * You may override this method if you want to use a different redis connection.
- * @return ARedisConnection the redis connection used by redis record.
- */
- public function getRedisConnection()
- {
- if ($this->_connection !== null) {
- return $this->_connection;
- }
- elseif(self::$redis!==null) {
- return self::$redis;
- }
- else
- {
- self::$redis=Yii::app()->redis;
- if(self::$redis instanceof ARedisConnection)
- return self::$redis;
- else
- throw new CException(Yii::t('yii','Redis Record requires a "redis" ARedisConnection application component.'));
- }
- }
- /**
- * Sets the redis connection used by this redis record
- * @param ARedisConnection $connection the redis connection to use for this record
- */
- public function setRedisConnection(ARedisConnection $connection) {
- $this->_connection = $connection;
- }
- /**
- * Creates a redis record instance.
- * This method is called by {@link populateRecord} and {@link populateRecords}.
- * You may override this method if the instance being created
- * depends the attributes that are to be populated to the record.
- * For example, by creating a record based on the value of a column,
- * you may implement the so-called single-table inheritance mapping.
- * @param array $attributes list of attribute values for the redis record.
- * @return ARedisRecord the active record
- */
- protected function instantiate($attributes)
- {
- $class=get_class($this);
- $model=new $class(null);
- return $model;
- }
- /**
- * Creates a redis record with the given attributes.
- * This method is internally used by the find methods.
- * @param array $attributes attribute values (column name=>column value)
- * @param boolean $callAfterFind whether to call {@link afterFind} after the record is populated.
- * @return ARedisRecord the newly created redis record. The class of the object is the same as the model class.
- * Null is returned if the input data is false.
- */
- public function populateRecord($attributes,$callAfterFind=true)
- {
- if($attributes!==false)
- {
- $record=$this->instantiate($attributes);
- $record->setScenario('update');
- $record->init();
- foreach($attributes as $name=>$value) {
- if (property_exists($record,$name)) {
- $record->$name=$value;
- }
- }
- $record->_pk=$record->getPrimaryKey();
- $record->attachBehaviors($record->behaviors());
- if($callAfterFind) {
- $record->afterFind();
- }
- return $record;
- }
- else {
- return null;
- }
- }
- /**
- * Creates a list of redis records based on the input data.
- * This method is internally used by the find methods.
- * @param array $data list of attribute values for the redis records.
- * @param boolean $callAfterFind whether to call {@link afterFind} after each record is populated.
- * @param string $index the name of the attribute whose value will be used as indexes of the query result array.
- * If null, it means the array will be indexed by zero-based integers.
- * @return array list of redis records.
- */
- public function populateRecords($data,$callAfterFind=true,$index=null)
- {
- $records=array();
- foreach($data as $attributes)
- {
- if(($record=$this->populateRecord($attributes,$callAfterFind))!==null)
- {
- if($index===null)
- $records[]=$record;
- else
- $records[$record->$index]=$record;
- }
- }
- return $records;
- }
- /**
- * Returns the name of the primary key of the associated redis index.
- * Child classes should override this if the primary key is anything other than "id"
- * @return mixed the primary key attribute name(s). Defaults to "id"
- */
- public function primaryKey()
- {
- return "id";
- }
- /**
- * Gets the redis key used when storing the attributes for this model
- * @param mixed $pk the primary key to create the redis key for
- * @return string the redis key
- */
- public function getRedisKey($pk = null) {
- if ($pk === null) {
- $pk = $this->getPrimaryKey();
- }
- if (is_array($pk)) {
- foreach($pk as $key => $value) {
- $pk[$key] = $key.":".$value;
- }
- $pk = implode(":",$pk);
- }
- return get_class($this).":".$pk;
- }
- /**
- * Returns the primary key value.
- * @return mixed the primary key value. An array (column name=>column value) is returned if the primary key is composite.
- * If primary key is not defined, null will be returned.
- */
- public function getPrimaryKey()
- {
- $attribute = $this->primaryKey();
- if (!is_array($attribute)) {
- return $this->{$attribute};
- }
- $pk = array();
- foreach($attribute as $field) {
- $pk[$field] = $this->{$attribute};
- }
- return $pk;
- }
- /**
- * Sets the primary key value.
- * After calling this method, the old primary key value can be obtained from {@link oldPrimaryKey}.
- * @param mixed $value the new primary key value. If the primary key is composite, the new value
- * should be provided as an array (column name=>column value).
- */
- public function setPrimaryKey($value)
- {
- $this->_pk=$this->getPrimaryKey();
- $attribute = $this->primaryKey();
- if (!is_array($attribute)) {
- return $this->{$attribute} = $value;
- }
- foreach($value as $attribute => $attributeValue) {
- $this->{$attribute} = $attributeValue;
- }
- return $value;
- }
- /**
- * Returns the old primary key value.
- * This refers to the primary key value that is populated into the record
- * after executing a find method (e.g. find(), findAll()).
- * The value remains unchanged even if the primary key attribute is manually assigned with a different value.
- * @return mixed the old primary key value. An array (column name=>column value) is returned if the primary key is composite.
- * If primary key is not defined, null will be returned.
- * @since 1.1.0
- */
- public function getOldPrimaryKey()
- {
- return $this->_pk;
- }
- /**
- * Sets the old primary key value.
- * @param mixed $value the old primary key value.
- * @since 1.1.3
- */
- public function setOldPrimaryKey($value)
- {
- $this->_pk=$value;
- }
- /**
- * Saves the redis record
- * @param boolean $runValidation whether to run validation or not, defaults to true
- * @return boolean whether the save succeeded or not
- */
- public function save($runValidation = true) {
- if ($runValidation && !$this->validate()) {
- return false;
- }
- if (!$this->beforeSave()) {
- return false;
- }
- if ($this->getPrimaryKey() === null) {
- $count = $this->getRedisSet()->getCount();
- $this->setPrimaryKey($count);
- while (!$this->getRedisSet()->add($this->getRedisKey())) {
- $count++;
- $this->setPrimaryKey($count); // try again, this is suboptimal, need a better way to avoid collisions
- }
- }
- elseif($this->getIsNewRecord() && !$this->getRedisSet()->add($this->getRedisKey())) {
- $this->addError($this->primaryKey(),"A record with this id already exists");
- return false;
- }
- $this->getRedisConnection()->getClient()->multi(); // enter transactional mode
- $this->getRedisHash()->clear();
- foreach($this->attributeNames() as $attribute) {
- $this->getRedisHash()->add($attribute, $this->{$attribute});
- }
- $this->getRedisConnection()->getClient()->exec();
- $this->afterSave();
- return true;
- }
- /**
- * Deletes the redis record
- * @return boolean whether the delete succeeded or not
- */
- public function delete() {
- if (!$this->beforeDelete()){
- return false;
- }
- $this->getRedisSet()->remove($this->getRedisKey());
- $this->getRedisHash()->clear();
- $this->afterDelete();
- return true;
- }
- /**
- * Returns the number of records of this type
- * @return integer the number of rows found
- */
- public function count()
- {
- Yii::trace(get_class($this).'.count()','packages.redis.ARedisRecord');
- return $this->getRedisSet()->getCount();
- }
- /**
- * Finds a single redis record with the specified primary key.
- * @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).
- * @return ARedisRecord the record found. Null if none is found.
- */
- public function findByPk($pk)
- {
- Yii::trace(get_class($this).'.findByPk()','packages.redis.ARedisRecord');
- $this->beforeFind();
- $hash = new ARedisHash($this->getRedisKey($pk),$this->getRedisConnection());
- if ($hash->getCount() == 0) {
- return null;
- }
- return $this->populateRecord($hash->toArray(),true);
- }
- /**
- * Finds multiple redis records with the specified primary keys.
- * @param array $pks primary key values.
- * @return ARedisRecord[] the records found.
- */
- public function findAllByPk($pks)
- {
- Yii::trace(get_class($this).'.findAllByPk()','packages.redis.ARedisRecord');
- $hashes = array();
- $redis = $this->getRedisConnection()->getClient()->multi();
- foreach($pks as $pk) {
- $key = $this->getRedisKey($pk);
- $redis->hGetAll($key);
- }
- $response = $redis->exec();
- $rows = array();
- foreach($response as $row) {
- if (!$row || !count($row)) {
- continue;
- }
- $rows[] = $row;
- }
- return $this->populateRecords($rows,true);
- }
- /**
- * Returns if the current record is new.
- * @return boolean whether the record is new and should be inserted when calling {@link save}.
- * This property is automatically set in constructor and {@link populateRecord}.
- * Defaults to false, but it will be set to true if the instance is created using
- * the new operator.
- */
- public function getIsNewRecord()
- {
- return $this->_new;
- }
- /**
- * Sets if the record is new.
- * @param boolean $value whether the record is new and should be inserted when calling {@link save}.
- * @see getIsNewRecord
- */
- public function setIsNewRecord($value)
- {
- $this->_new=$value;
- }
- /**
- * This event is raised before the record is saved.
- * By setting {@link CModelEvent::isValid} to be false, the normal {@link save()} process will be stopped.
- * @param CModelEvent $event the event parameter
- */
- public function onBeforeSave($event)
- {
- $this->raiseEvent('onBeforeSave',$event);
- }
- /**
- * This event is raised after the record is saved.
- * @param CEvent $event the event parameter
- */
- public function onAfterSave($event)
- {
- $this->raiseEvent('onAfterSave',$event);
- }
- /**
- * This event is raised before the record is deleted.
- * By setting {@link CModelEvent::isValid} to be false, the normal {@link delete()} process will be stopped.
- * @param CModelEvent $event the event parameter
- */
- public function onBeforeDelete($event)
- {
- $this->raiseEvent('onBeforeDelete',$event);
- }
- /**
- * This event is raised after the record is deleted.
- * @param CEvent $event the event parameter
- */
- public function onAfterDelete($event)
- {
- $this->raiseEvent('onAfterDelete',$event);
- }
- /**
- * This event is raised before a find call.
- * In this event, the {@link CModelEvent::criteria} property contains the query criteria
- * passed as parameters to those find methods. If you want to access
- * the query criteria specified in scopes, please use {@link getDbCriteria()}.
- * You can modify either criteria to customize them based on needs.
- * @param CModelEvent $event the event parameter
- * @see beforeFind
- */
- public function onBeforeFind($event)
- {
- $this->raiseEvent('onBeforeFind',$event);
- }
- /**
- * This event is raised after the record is instantiated by a find method.
- * @param CEvent $event the event parameter
- */
- public function onAfterFind($event)
- {
- $this->raiseEvent('onAfterFind',$event);
- }
- /**
- * This method is invoked before saving a record (after validation, if any).
- * The default implementation raises the {@link onBeforeSave} event.
- * You may override this method to do any preparation work for record saving.
- * Use {@link isNewRecord} to determine whether the saving is
- * for inserting or updating record.
- * Make sure you call the parent implementation so that the event is raised properly.
- * @return boolean whether the saving should be executed. Defaults to true.
- */
- protected function beforeSave()
- {
- if($this->hasEventHandler('onBeforeSave'))
- {
- $event=new CModelEvent($this);
- $this->onBeforeSave($event);
- return $event->isValid;
- }
- else
- return true;
- }
- /**
- * This method is invoked after saving a record successfully.
- * The default implementation raises the {@link onAfterSave} event.
- * You may override this method to do postprocessing after record saving.
- * Make sure you call the parent implementation so that the event is raised properly.
- */
- protected function afterSave()
- {
- if($this->hasEventHandler('onAfterSave'))
- $this->onAfterSave(new CEvent($this));
- }
- /**
- * This method is invoked before deleting a record.
- * The default implementation raises the {@link onBeforeDelete} event.
- * You may override this method to do any preparation work for record deletion.
- * Make sure you call the parent implementation so that the event is raised properly.
- * @return boolean whether the record should be deleted. Defaults to true.
- */
- protected function beforeDelete()
- {
- if($this->hasEventHandler('onBeforeDelete'))
- {
- $event=new CModelEvent($this);
- $this->onBeforeDelete($event);
- return $event->isValid;
- }
- else
- return true;
- }
- /**
- * This method is invoked after deleting a record.
- * The default implementation raises the {@link onAfterDelete} event.
- * You may override this method to do postprocessing after the record is deleted.
- * Make sure you call the parent implementation so that the event is raised properly.
- */
- protected function afterDelete()
- {
- if($this->hasEventHandler('onAfterDelete'))
- $this->onAfterDelete(new CEvent($this));
- }
- /**
- * This method is invoked before a find call.
- * The find calls include {@link find}, {@link findAll}, {@link findByPk},
- * {@link findAllByPk}, {@link findByAttributes} and {@link findAllByAttributes}.
- * The default implementation raises the {@link onBeforeFind} event.
- * If you override this method, make sure you call the parent implementation
- * so that the event is raised properly.
- */
- protected function beforeFind()
- {
- if($this->hasEventHandler('onBeforeFind'))
- {
- $event=new CModelEvent($this);
- // for backward compatibility
- $event->criteria=func_num_args()>0 ? func_get_arg(0) : null;
- $this->onBeforeFind($event);
- }
- }
- /**
- * This method is invoked after each record is instantiated by a find method.
- * The default implementation raises the {@link onAfterFind} event.
- * You may override this method to do postprocessing after each newly found record is instantiated.
- * Make sure you call the parent implementation so that the event is raised properly.
- */
- protected function afterFind()
- {
- if($this->hasEventHandler('onAfterFind'))
- $this->onAfterFind(new CEvent($this));
- }
- /**
- * Calls {@link beforeFind}.
- * This method is internally used.
- * @since 1.0.11
- */
- public function beforeFindInternal()
- {
- $this->beforeFind();
- }
- /**
- * Calls {@link afterFind}.
- * This method is internally used.
- * @since 1.0.3
- */
- public function afterFindInternal()
- {
- $this->afterFind();
- }
- /**
- * Sets the redis hash to use with this record
- * @param ARedisIterableHash $redisHash the redis hash
- */
- public function setRedisHash($redisHash)
- {
- $this->_redisHash = $redisHash;
- }
- /**
- * Gets the redis hash to store the attributes for this record in
- * @return ARedisIterableHash the redis hash
- */
- public function getRedisHash()
- {
- if ($this->_redisHash === null) {
- $this->_redisHash = new ARedisHash($this->getRedisKey(), $this->getRedisConnection());
- }
- return $this->_redisHash;
- }
- /**
- * Sets the redis set that contains the ids of the models of this type
- * @param ARedisIterableSet $redisSet the redis set
- */
- public function setRedisSet($redisSet)
- {
- $this->_redisSet = $redisSet;
- }
- /**
- * Gets the redis set that contains the ids of the models of this type
- * @return ARedisIterableSet the redis set
- */
- public function getRedisSet()
- {
- if ($this->_redisSet === null) {
- $this->_redisSet = new ARedisSet(get_class($this), $this->getRedisConnection());
- }
- return $this->_redisSet;
- }
- }
|