ARedisRecord.php 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644
  1. <?php
  2. /**
  3. * Allows access to redis records using the active record pattern.
  4. * <pre>
  5. * $record = ARedisRecord::model()->findByPk(1); // loads a record with a unique id of 1
  6. * $record->name = "a test name"; // sets the name attribute on the record
  7. * $record->save(); // saves the record to redis
  8. * $record->delete(); // deletes the record from redis
  9. * </pre>
  10. * @author Charles Pick / PeoplePerHour.com
  11. * @package packages.redis
  12. */
  13. abstract class ARedisRecord extends CFormModel {
  14. /**
  15. * The redis connection
  16. * @var ARedisConnection
  17. */
  18. public static $redis;
  19. /**
  20. * The record attributes.
  21. * @var CAttributeCollection
  22. */
  23. protected $_attributes;
  24. /**
  25. * The connection to redis
  26. * @var ARedisConnection
  27. */
  28. protected $_connection;
  29. /**
  30. * The redis set that represents the list of models of this type
  31. * @var ARedisIterableSet
  32. */
  33. protected $_redisSet;
  34. /**
  35. * The redis hash that contains the values for this record
  36. * @var ARedisIterableHash
  37. */
  38. protected $_redisHash;
  39. /**
  40. * The old primary key value
  41. * @var mixed
  42. */
  43. private $_pk;
  44. /**
  45. * Whether this is a new record or not
  46. * @var boolean
  47. */
  48. private $_new = true;
  49. /**
  50. * An array of static model instances, clas name => model
  51. * @var array
  52. */
  53. private static $_models=array();
  54. /**
  55. * Constructor.
  56. * @param string $scenario the scenario name
  57. * See {@link CModel::scenario} on how scenario is used by models.
  58. * @see getScenario
  59. */
  60. public function __construct($scenario = "insert")
  61. {
  62. if ($scenario === null) {
  63. return;
  64. }
  65. $this->init();
  66. $this->attachBehaviors($this->behaviors());
  67. $this->afterConstruct();
  68. }
  69. /**
  70. * Returns the static model of the specified redis record class.
  71. * The model returned is a static instance of the redis record class.
  72. * It is provided for invoking class-level methods (something similar to static class methods.)
  73. *
  74. * EVERY derived redis record class must override this method as follows,
  75. * <pre>
  76. * public static function model($className=__CLASS__)
  77. * {
  78. * return parent::model($className);
  79. * }
  80. * </pre>
  81. *
  82. * @param string $className redis record class name.
  83. * @return ARedisRecord redis record model instance.
  84. */
  85. public static function model($className=__CLASS__)
  86. {
  87. if(isset(self::$_models[$className]))
  88. return self::$_models[$className];
  89. else
  90. {
  91. $model=self::$_models[$className]=new $className(null);
  92. $model->attachBehaviors($model->behaviors());
  93. return $model;
  94. }
  95. }
  96. /**
  97. * Returns the redis connection used by redis record.
  98. * By default, the "redis" application component is used as the redis connection.
  99. * You may override this method if you want to use a different redis connection.
  100. * @return ARedisConnection the redis connection used by redis record.
  101. */
  102. public function getRedisConnection()
  103. {
  104. if ($this->_connection !== null) {
  105. return $this->_connection;
  106. }
  107. elseif(self::$redis!==null) {
  108. return self::$redis;
  109. }
  110. else
  111. {
  112. self::$redis=Yii::app()->redis;
  113. if(self::$redis instanceof ARedisConnection)
  114. return self::$redis;
  115. else
  116. throw new CException(Yii::t('yii','Redis Record requires a "redis" ARedisConnection application component.'));
  117. }
  118. }
  119. /**
  120. * Sets the redis connection used by this redis record
  121. * @param ARedisConnection $connection the redis connection to use for this record
  122. */
  123. public function setRedisConnection(ARedisConnection $connection) {
  124. $this->_connection = $connection;
  125. }
  126. /**
  127. * Creates a redis record instance.
  128. * This method is called by {@link populateRecord} and {@link populateRecords}.
  129. * You may override this method if the instance being created
  130. * depends the attributes that are to be populated to the record.
  131. * For example, by creating a record based on the value of a column,
  132. * you may implement the so-called single-table inheritance mapping.
  133. * @param array $attributes list of attribute values for the redis record.
  134. * @return ARedisRecord the active record
  135. */
  136. protected function instantiate($attributes)
  137. {
  138. $class=get_class($this);
  139. $model=new $class(null);
  140. return $model;
  141. }
  142. /**
  143. * Creates a redis record with the given attributes.
  144. * This method is internally used by the find methods.
  145. * @param array $attributes attribute values (column name=>column value)
  146. * @param boolean $callAfterFind whether to call {@link afterFind} after the record is populated.
  147. * @return ARedisRecord the newly created redis record. The class of the object is the same as the model class.
  148. * Null is returned if the input data is false.
  149. */
  150. public function populateRecord($attributes,$callAfterFind=true)
  151. {
  152. if($attributes!==false)
  153. {
  154. $record=$this->instantiate($attributes);
  155. $record->setScenario('update');
  156. $record->init();
  157. foreach($attributes as $name=>$value) {
  158. if (property_exists($record,$name)) {
  159. $record->$name=$value;
  160. }
  161. }
  162. $record->_pk=$record->getPrimaryKey();
  163. $record->attachBehaviors($record->behaviors());
  164. if($callAfterFind) {
  165. $record->afterFind();
  166. }
  167. return $record;
  168. }
  169. else {
  170. return null;
  171. }
  172. }
  173. /**
  174. * Creates a list of redis records based on the input data.
  175. * This method is internally used by the find methods.
  176. * @param array $data list of attribute values for the redis records.
  177. * @param boolean $callAfterFind whether to call {@link afterFind} after each record is populated.
  178. * @param string $index the name of the attribute whose value will be used as indexes of the query result array.
  179. * If null, it means the array will be indexed by zero-based integers.
  180. * @return array list of redis records.
  181. */
  182. public function populateRecords($data,$callAfterFind=true,$index=null)
  183. {
  184. $records=array();
  185. foreach($data as $attributes)
  186. {
  187. if(($record=$this->populateRecord($attributes,$callAfterFind))!==null)
  188. {
  189. if($index===null)
  190. $records[]=$record;
  191. else
  192. $records[$record->$index]=$record;
  193. }
  194. }
  195. return $records;
  196. }
  197. /**
  198. * Returns the name of the primary key of the associated redis index.
  199. * Child classes should override this if the primary key is anything other than "id"
  200. * @return mixed the primary key attribute name(s). Defaults to "id"
  201. */
  202. public function primaryKey()
  203. {
  204. return "id";
  205. }
  206. /**
  207. * Gets the redis key used when storing the attributes for this model
  208. * @param mixed $pk the primary key to create the redis key for
  209. * @return string the redis key
  210. */
  211. public function getRedisKey($pk = null) {
  212. if ($pk === null) {
  213. $pk = $this->getPrimaryKey();
  214. }
  215. if (is_array($pk)) {
  216. foreach($pk as $key => $value) {
  217. $pk[$key] = $key.":".$value;
  218. }
  219. $pk = implode(":",$pk);
  220. }
  221. return get_class($this).":".$pk;
  222. }
  223. /**
  224. * Returns the primary key value.
  225. * @return mixed the primary key value. An array (column name=>column value) is returned if the primary key is composite.
  226. * If primary key is not defined, null will be returned.
  227. */
  228. public function getPrimaryKey()
  229. {
  230. $attribute = $this->primaryKey();
  231. if (!is_array($attribute)) {
  232. return $this->{$attribute};
  233. }
  234. $pk = array();
  235. foreach($attribute as $field) {
  236. $pk[$field] = $this->{$attribute};
  237. }
  238. return $pk;
  239. }
  240. /**
  241. * Sets the primary key value.
  242. * After calling this method, the old primary key value can be obtained from {@link oldPrimaryKey}.
  243. * @param mixed $value the new primary key value. If the primary key is composite, the new value
  244. * should be provided as an array (column name=>column value).
  245. */
  246. public function setPrimaryKey($value)
  247. {
  248. $this->_pk=$this->getPrimaryKey();
  249. $attribute = $this->primaryKey();
  250. if (!is_array($attribute)) {
  251. return $this->{$attribute} = $value;
  252. }
  253. foreach($value as $attribute => $attributeValue) {
  254. $this->{$attribute} = $attributeValue;
  255. }
  256. return $value;
  257. }
  258. /**
  259. * Returns the old primary key value.
  260. * This refers to the primary key value that is populated into the record
  261. * after executing a find method (e.g. find(), findAll()).
  262. * The value remains unchanged even if the primary key attribute is manually assigned with a different value.
  263. * @return mixed the old primary key value. An array (column name=>column value) is returned if the primary key is composite.
  264. * If primary key is not defined, null will be returned.
  265. * @since 1.1.0
  266. */
  267. public function getOldPrimaryKey()
  268. {
  269. return $this->_pk;
  270. }
  271. /**
  272. * Sets the old primary key value.
  273. * @param mixed $value the old primary key value.
  274. * @since 1.1.3
  275. */
  276. public function setOldPrimaryKey($value)
  277. {
  278. $this->_pk=$value;
  279. }
  280. /**
  281. * Saves the redis record
  282. * @param boolean $runValidation whether to run validation or not, defaults to true
  283. * @return boolean whether the save succeeded or not
  284. */
  285. public function save($runValidation = true) {
  286. if ($runValidation && !$this->validate()) {
  287. return false;
  288. }
  289. if (!$this->beforeSave()) {
  290. return false;
  291. }
  292. if ($this->getPrimaryKey() === null) {
  293. $count = $this->getRedisSet()->getCount();
  294. $this->setPrimaryKey($count);
  295. while (!$this->getRedisSet()->add($this->getRedisKey())) {
  296. $count++;
  297. $this->setPrimaryKey($count); // try again, this is suboptimal, need a better way to avoid collisions
  298. }
  299. }
  300. elseif($this->getIsNewRecord() && !$this->getRedisSet()->add($this->getRedisKey())) {
  301. $this->addError($this->primaryKey(),"A record with this id already exists");
  302. return false;
  303. }
  304. $this->getRedisConnection()->getClient()->multi(); // enter transactional mode
  305. $this->getRedisHash()->clear();
  306. foreach($this->attributeNames() as $attribute) {
  307. $this->getRedisHash()->add($attribute, $this->{$attribute});
  308. }
  309. $this->getRedisConnection()->getClient()->exec();
  310. $this->afterSave();
  311. return true;
  312. }
  313. /**
  314. * Deletes the redis record
  315. * @return boolean whether the delete succeeded or not
  316. */
  317. public function delete() {
  318. if (!$this->beforeDelete()){
  319. return false;
  320. }
  321. $this->getRedisSet()->remove($this->getRedisKey());
  322. $this->getRedisHash()->clear();
  323. $this->afterDelete();
  324. return true;
  325. }
  326. /**
  327. * Returns the number of records of this type
  328. * @return integer the number of rows found
  329. */
  330. public function count()
  331. {
  332. Yii::trace(get_class($this).'.count()','packages.redis.ARedisRecord');
  333. return $this->getRedisSet()->getCount();
  334. }
  335. /**
  336. * Finds a single redis record with the specified primary key.
  337. * @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).
  338. * @return ARedisRecord the record found. Null if none is found.
  339. */
  340. public function findByPk($pk)
  341. {
  342. Yii::trace(get_class($this).'.findByPk()','packages.redis.ARedisRecord');
  343. $this->beforeFind();
  344. $hash = new ARedisHash($this->getRedisKey($pk),$this->getRedisConnection());
  345. if ($hash->getCount() == 0) {
  346. return null;
  347. }
  348. return $this->populateRecord($hash->toArray(),true);
  349. }
  350. /**
  351. * Finds multiple redis records with the specified primary keys.
  352. * @param array $pks primary key values.
  353. * @return ARedisRecord[] the records found.
  354. */
  355. public function findAllByPk($pks)
  356. {
  357. Yii::trace(get_class($this).'.findAllByPk()','packages.redis.ARedisRecord');
  358. $hashes = array();
  359. $redis = $this->getRedisConnection()->getClient()->multi();
  360. foreach($pks as $pk) {
  361. $key = $this->getRedisKey($pk);
  362. $redis->hGetAll($key);
  363. }
  364. $response = $redis->exec();
  365. $rows = array();
  366. foreach($response as $row) {
  367. if (!$row || !count($row)) {
  368. continue;
  369. }
  370. $rows[] = $row;
  371. }
  372. return $this->populateRecords($rows,true);
  373. }
  374. /**
  375. * Returns if the current record is new.
  376. * @return boolean whether the record is new and should be inserted when calling {@link save}.
  377. * This property is automatically set in constructor and {@link populateRecord}.
  378. * Defaults to false, but it will be set to true if the instance is created using
  379. * the new operator.
  380. */
  381. public function getIsNewRecord()
  382. {
  383. return $this->_new;
  384. }
  385. /**
  386. * Sets if the record is new.
  387. * @param boolean $value whether the record is new and should be inserted when calling {@link save}.
  388. * @see getIsNewRecord
  389. */
  390. public function setIsNewRecord($value)
  391. {
  392. $this->_new=$value;
  393. }
  394. /**
  395. * This event is raised before the record is saved.
  396. * By setting {@link CModelEvent::isValid} to be false, the normal {@link save()} process will be stopped.
  397. * @param CModelEvent $event the event parameter
  398. */
  399. public function onBeforeSave($event)
  400. {
  401. $this->raiseEvent('onBeforeSave',$event);
  402. }
  403. /**
  404. * This event is raised after the record is saved.
  405. * @param CEvent $event the event parameter
  406. */
  407. public function onAfterSave($event)
  408. {
  409. $this->raiseEvent('onAfterSave',$event);
  410. }
  411. /**
  412. * This event is raised before the record is deleted.
  413. * By setting {@link CModelEvent::isValid} to be false, the normal {@link delete()} process will be stopped.
  414. * @param CModelEvent $event the event parameter
  415. */
  416. public function onBeforeDelete($event)
  417. {
  418. $this->raiseEvent('onBeforeDelete',$event);
  419. }
  420. /**
  421. * This event is raised after the record is deleted.
  422. * @param CEvent $event the event parameter
  423. */
  424. public function onAfterDelete($event)
  425. {
  426. $this->raiseEvent('onAfterDelete',$event);
  427. }
  428. /**
  429. * This event is raised before a find call.
  430. * In this event, the {@link CModelEvent::criteria} property contains the query criteria
  431. * passed as parameters to those find methods. If you want to access
  432. * the query criteria specified in scopes, please use {@link getDbCriteria()}.
  433. * You can modify either criteria to customize them based on needs.
  434. * @param CModelEvent $event the event parameter
  435. * @see beforeFind
  436. */
  437. public function onBeforeFind($event)
  438. {
  439. $this->raiseEvent('onBeforeFind',$event);
  440. }
  441. /**
  442. * This event is raised after the record is instantiated by a find method.
  443. * @param CEvent $event the event parameter
  444. */
  445. public function onAfterFind($event)
  446. {
  447. $this->raiseEvent('onAfterFind',$event);
  448. }
  449. /**
  450. * This method is invoked before saving a record (after validation, if any).
  451. * The default implementation raises the {@link onBeforeSave} event.
  452. * You may override this method to do any preparation work for record saving.
  453. * Use {@link isNewRecord} to determine whether the saving is
  454. * for inserting or updating record.
  455. * Make sure you call the parent implementation so that the event is raised properly.
  456. * @return boolean whether the saving should be executed. Defaults to true.
  457. */
  458. protected function beforeSave()
  459. {
  460. if($this->hasEventHandler('onBeforeSave'))
  461. {
  462. $event=new CModelEvent($this);
  463. $this->onBeforeSave($event);
  464. return $event->isValid;
  465. }
  466. else
  467. return true;
  468. }
  469. /**
  470. * This method is invoked after saving a record successfully.
  471. * The default implementation raises the {@link onAfterSave} event.
  472. * You may override this method to do postprocessing after record saving.
  473. * Make sure you call the parent implementation so that the event is raised properly.
  474. */
  475. protected function afterSave()
  476. {
  477. if($this->hasEventHandler('onAfterSave'))
  478. $this->onAfterSave(new CEvent($this));
  479. }
  480. /**
  481. * This method is invoked before deleting a record.
  482. * The default implementation raises the {@link onBeforeDelete} event.
  483. * You may override this method to do any preparation work for record deletion.
  484. * Make sure you call the parent implementation so that the event is raised properly.
  485. * @return boolean whether the record should be deleted. Defaults to true.
  486. */
  487. protected function beforeDelete()
  488. {
  489. if($this->hasEventHandler('onBeforeDelete'))
  490. {
  491. $event=new CModelEvent($this);
  492. $this->onBeforeDelete($event);
  493. return $event->isValid;
  494. }
  495. else
  496. return true;
  497. }
  498. /**
  499. * This method is invoked after deleting a record.
  500. * The default implementation raises the {@link onAfterDelete} event.
  501. * You may override this method to do postprocessing after the record is deleted.
  502. * Make sure you call the parent implementation so that the event is raised properly.
  503. */
  504. protected function afterDelete()
  505. {
  506. if($this->hasEventHandler('onAfterDelete'))
  507. $this->onAfterDelete(new CEvent($this));
  508. }
  509. /**
  510. * This method is invoked before a find call.
  511. * The find calls include {@link find}, {@link findAll}, {@link findByPk},
  512. * {@link findAllByPk}, {@link findByAttributes} and {@link findAllByAttributes}.
  513. * The default implementation raises the {@link onBeforeFind} event.
  514. * If you override this method, make sure you call the parent implementation
  515. * so that the event is raised properly.
  516. */
  517. protected function beforeFind()
  518. {
  519. if($this->hasEventHandler('onBeforeFind'))
  520. {
  521. $event=new CModelEvent($this);
  522. // for backward compatibility
  523. $event->criteria=func_num_args()>0 ? func_get_arg(0) : null;
  524. $this->onBeforeFind($event);
  525. }
  526. }
  527. /**
  528. * This method is invoked after each record is instantiated by a find method.
  529. * The default implementation raises the {@link onAfterFind} event.
  530. * You may override this method to do postprocessing after each newly found record is instantiated.
  531. * Make sure you call the parent implementation so that the event is raised properly.
  532. */
  533. protected function afterFind()
  534. {
  535. if($this->hasEventHandler('onAfterFind'))
  536. $this->onAfterFind(new CEvent($this));
  537. }
  538. /**
  539. * Calls {@link beforeFind}.
  540. * This method is internally used.
  541. * @since 1.0.11
  542. */
  543. public function beforeFindInternal()
  544. {
  545. $this->beforeFind();
  546. }
  547. /**
  548. * Calls {@link afterFind}.
  549. * This method is internally used.
  550. * @since 1.0.3
  551. */
  552. public function afterFindInternal()
  553. {
  554. $this->afterFind();
  555. }
  556. /**
  557. * Sets the redis hash to use with this record
  558. * @param ARedisIterableHash $redisHash the redis hash
  559. */
  560. public function setRedisHash($redisHash)
  561. {
  562. $this->_redisHash = $redisHash;
  563. }
  564. /**
  565. * Gets the redis hash to store the attributes for this record in
  566. * @return ARedisIterableHash the redis hash
  567. */
  568. public function getRedisHash()
  569. {
  570. if ($this->_redisHash === null) {
  571. $this->_redisHash = new ARedisHash($this->getRedisKey(), $this->getRedisConnection());
  572. }
  573. return $this->_redisHash;
  574. }
  575. /**
  576. * Sets the redis set that contains the ids of the models of this type
  577. * @param ARedisIterableSet $redisSet the redis set
  578. */
  579. public function setRedisSet($redisSet)
  580. {
  581. $this->_redisSet = $redisSet;
  582. }
  583. /**
  584. * Gets the redis set that contains the ids of the models of this type
  585. * @return ARedisIterableSet the redis set
  586. */
  587. public function getRedisSet()
  588. {
  589. if ($this->_redisSet === null) {
  590. $this->_redisSet = new ARedisSet(get_class($this), $this->getRedisConnection());
  591. }
  592. return $this->_redisSet;
  593. }
  594. }