EMongoDocument.php 42 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497
  1. <?php
  2. /**
  3. * EMongoDocument
  4. *
  5. * The active record itself
  6. */
  7. class EMongoDocument extends EMongoModel
  8. {
  9. /**
  10. * Holds a set of cached models for the active record to instantiate from
  11. *
  12. * Whenever you call ::model() it will either find the class in this cache array and use it or will
  13. * make a whole new class and cache it into this array
  14. *
  15. * @var array
  16. */
  17. private static $_models = array();
  18. /**
  19. * Whether or not the document is new
  20. * @var boolean
  21. */
  22. private $_new = false;
  23. /**
  24. * Holds criteria information for scopes
  25. * @var array|null
  26. */
  27. private $_criteria;
  28. /**
  29. * Contains a list of fields that were projected, will only be taken into consideration
  30. * should _partial be true
  31. * @var array|string[]
  32. */
  33. private $_projected_fields = array();
  34. /**
  35. * A bit deceptive, this var actually holds the last response from the server. The reason why it is called this
  36. * is because this is what MongoDB calls it.
  37. * @var array
  38. */
  39. private $lastError;
  40. /**
  41. * Sets up our model and set the field cache just like in EMongoModel
  42. *
  43. * It will also set the default scope on the model so be aware that if you want the default scope to not be applied you will
  44. * need to run resetScope() straight after making this model
  45. *
  46. * @param string $scenario
  47. */
  48. public function __construct($scenario = 'insert')
  49. {
  50. $this->getDbConnection()->setDocumentCache($this);
  51. if($scenario === null){ // internally used by populateRecord() and model()
  52. return;
  53. }
  54. $this->setScenario($scenario);
  55. $this->setIsNewRecord(true);
  56. $this->init();
  57. $this->attachBehaviors($this->behaviors());
  58. $this->afterConstruct();
  59. }
  60. /**
  61. * This, in addition to EMongoModels edition, will also call scopes on the model
  62. * @see EMongoModel::__call()
  63. * @param string $name
  64. * @param array $parameters
  65. * @return EMongoDocument|mixed
  66. */
  67. public function __call($name, $parameters){
  68. if(array_key_exists($name, $this->relations())){
  69. if(empty($parameters)){
  70. return $this->getRelated($name, false);
  71. }
  72. return $this->getRelated($name, false, $parameters[0]);
  73. }
  74. $scopes = $this->scopes();
  75. if(isset($scopes[$name])){
  76. $this->setDbCriteria($this->mergeCriteria($this->getDbCriteria(), $scopes[$name]));
  77. return $this;
  78. }
  79. return parent::__call($name, $parameters);
  80. }
  81. /**
  82. * The scope attached to this model
  83. *
  84. * It is very much like how Yii normally uses scopes except the params are slightly different.
  85. *
  86. * @example
  87. *
  88. * array(
  89. * 'ten_recently_published' => array(
  90. * 'condition' => array('published' => 1),
  91. * 'sort' => array('date_published' => -1),
  92. * 'skip' => 5,
  93. * 'limit' => 10
  94. * )
  95. * )
  96. *
  97. * Not all params need to be defined they are all just there above to give an indea of how to use this
  98. *
  99. * @return array
  100. */
  101. public function scopes()
  102. {
  103. return array();
  104. }
  105. /**
  106. * Sets the default scope
  107. *
  108. * @example
  109. *
  110. * array(
  111. * 'condition' => array('published' => 1),
  112. * 'sort' => array('date_published' => -1),
  113. * 'skip' => 5,
  114. * 'limit' => 10
  115. * )
  116. *
  117. * @return array - An array which represents a single scope within the scope() function
  118. */
  119. public function defaultScope()
  120. {
  121. return array();
  122. }
  123. /**
  124. * Resets the scopes applied to the model clearing the _criteria variable
  125. * @param bool $resetDefault
  126. * @return EMongoDocument
  127. */
  128. public function resetScope($resetDefault = true)
  129. {
  130. $this->_criteria = ($resetDefault ? array() : null);
  131. return $this;
  132. }
  133. /**
  134. * Returns the collection name as a string
  135. *
  136. * @example
  137. *
  138. * return 'users';
  139. *
  140. * @return string
  141. */
  142. public function collectionName()
  143. {
  144. return get_class($this);
  145. }
  146. /**
  147. * Denotes whether or not this document is versioned
  148. * @return boolean
  149. */
  150. public function versioned()
  151. {
  152. return false;
  153. }
  154. /**
  155. * Denotes the field tob e used to house the version number
  156. * @return string
  157. */
  158. public function versionField()
  159. {
  160. return '_v';
  161. }
  162. /**
  163. * Returns MongoId based on $value
  164. *
  165. * @deprecated This function will become deprecated in favour of consistently
  166. * using the getPrimaryKey() function instead. Atm, however, the getPrimaryKey
  167. * function actually chains onto this method. If you see this and are wondering
  168. * about what you should do if you want custom primary keys etc just use the getPrimaryKey
  169. * function as you would the getMongoId function. These two functions should never have been separate
  170. * for they are the same essentially.
  171. *
  172. * As to what version this will become deprecated:- I dunno. It will not be soon since it will be a
  173. * functionality breaker...
  174. *
  175. * @param string|MongoId $value
  176. * @return MongoId
  177. */
  178. public function getMongoId($value = null)
  179. {
  180. return $value instanceof MongoId ? $value : new MongoId($value);
  181. }
  182. /**
  183. * Returns the value of the primary key
  184. * @param string|MongoId $value
  185. * @return MongoId
  186. */
  187. public function getPrimaryKey($value = null)
  188. {
  189. if($value === null){
  190. $value = $this->{$this->primaryKey()};
  191. }
  192. return $this->getMongoId($value);
  193. }
  194. /**
  195. * Returns if the current record is new.
  196. * Whether the record is new and should be inserted when calling {@link save}.
  197. * This property is automatically set in constructor and {@link populateRecord}.
  198. * Defaults to false, but it will be set to true if the instance is created using
  199. * the new operator.
  200. * @return boolean
  201. */
  202. public function getIsNewRecord()
  203. {
  204. return $this->_new;
  205. }
  206. /**
  207. * Sets if the record is new.
  208. * Whether the record is new and should be inserted when calling {@link save}.
  209. * @see EMongoDocument::getIsNewRecord()
  210. * @param boolean $value
  211. */
  212. public function setIsNewRecord($value)
  213. {
  214. $this->_new = (bool)$value;
  215. }
  216. /**
  217. * Gets a list of the projected fields for the model
  218. * @return array|string[]
  219. */
  220. public function getProjectedFields()
  221. {
  222. return $this->_projected_fields;
  223. }
  224. /**
  225. * Sets the projected fields of the model
  226. * @param array|string[] $fields
  227. */
  228. public function setProjectedFields(array $fields)
  229. {
  230. $this->_projected_fields = $fields;
  231. }
  232. /**
  233. * Gets the version of this document
  234. * @return string
  235. */
  236. public function version()
  237. {
  238. return $this->{$this->versionField()};
  239. }
  240. /**
  241. * Forceably increments the version of this document
  242. * @return bool
  243. */
  244. public function incrementVersion()
  245. {
  246. $resp = $this->updateByPk($this->getPrimaryKey(), array('$inc' => array($this->versionField() => 1)));
  247. if($resp['n'] <= 0){
  248. return false;
  249. }
  250. $this->{$this->versionField()} += 1;
  251. return true;
  252. }
  253. /**
  254. * Forceably sets the version of this document
  255. * @param mixed $n
  256. * @return bool
  257. */
  258. public function setVersion($n)
  259. {
  260. $resp = $this->updateByPk($this->getPrimaryKey(), array('$set' => array($this->versionField() => $n)));
  261. if($resp['n'] <= 0){
  262. return false;
  263. }
  264. $this->{$this->versionField()} = $n;
  265. return true;
  266. }
  267. /**
  268. * Sets the attribute of the model
  269. * @param string $name
  270. * @param mixed $value
  271. * @return bool
  272. */
  273. public function setAttribute($name, $value)
  274. {
  275. // At the moment the projection is restricted to only fields returned in result set
  276. // Uncomment this to change that
  277. //if($this->getIsPartial())
  278. // $this->_projected_fields[$name] = 1;
  279. return parent::setAttribute($name, $value);
  280. }
  281. /**
  282. * Returns the static model of the specified AR class.
  283. * The model returned is a static instance of the AR class.
  284. * It is provided for invoking class-level methods (something similar to static class methods.)
  285. *
  286. * EVERY derived AR class must override this method as follows,
  287. * <pre>
  288. * public static function model($className=__CLASS__)
  289. * {
  290. * return parent::model($className);
  291. * }
  292. * </pre>
  293. *
  294. * @param string $className
  295. * @return EMongoDocument
  296. */
  297. public static function model($className = __CLASS__)
  298. {
  299. if(isset(self::$_models[$className])){
  300. return self::$_models[$className];
  301. }
  302. /** @var EMongoDocument $model */
  303. $model = self::$_models[$className] = new $className(null);
  304. $model->attachBehaviors($model->behaviors());
  305. return $model;
  306. }
  307. /**
  308. * Instantiates a model from an array
  309. * @param array $document
  310. * @return EMongoDocument
  311. */
  312. protected function instantiate($document)
  313. {
  314. $class = get_class($this);
  315. return new $class(null);
  316. }
  317. /**
  318. * Returns the text label for the specified attribute.
  319. * This method overrides the parent implementation by supporting
  320. * returning the label defined in relational object.
  321. * In particular, if the attribute name is in the form of "post.author.name",
  322. * then this method will derive the label from the "author" relation's "name" attribute.
  323. * @see CModel::generateAttributeLabel()
  324. * @param string $attribute - the attribute name
  325. * @return string - the attribute label
  326. */
  327. public function getAttributeLabel($attribute)
  328. {
  329. $labels = $this->attributeLabels();
  330. if(isset($labels[$attribute])){
  331. return $labels[$attribute];
  332. }
  333. if(strpos($attribute, '.') === false){
  334. return $this->generateAttributeLabel($attribute);
  335. }
  336. $segs = explode('.', $attribute);
  337. $name = array_pop($segs);
  338. $model = $this;
  339. foreach($segs as $seg){
  340. $relations = $model->relations();
  341. if(!isset($relations[$seg])){
  342. break;
  343. }
  344. $model = EMongoDocument::model($relations[$seg][1]);
  345. }
  346. return $model->getAttributeLabel($name);
  347. }
  348. /**
  349. * Creates an active record with the given attributes.
  350. * This method is internally used by the find methods.
  351. * Null is returned if the input data is false.
  352. *
  353. * @param array $attributes - attribute values (column name=>column value)
  354. * @param boolean $callAfterFind whether to call {@link afterFind} after the record is populated.
  355. * @param bool $partial
  356. * @return EMongoDocument|null - the newly created active record. The class of the object is the same as the model class.
  357. */
  358. public function populateRecord($attributes, $callAfterFind = true, $partial = false)
  359. {
  360. if($attributes === false || $attributes === null){
  361. return null;
  362. }
  363. $record = $this->instantiate($attributes);
  364. $record->setScenario('update');
  365. $record->setIsNewRecord(false);
  366. $record->init();
  367. $labels = array();
  368. foreach($attributes as $name=>$value){
  369. $labels[$name] = 1;
  370. $record->setAttribute($name, $value);
  371. }
  372. if($partial){
  373. $record->setIsPartial(true);
  374. $record->setProjectedFields($labels);
  375. }
  376. //$record->_pk=$record->primaryKey();
  377. $record->attachBehaviors($record->behaviors());
  378. if($callAfterFind){
  379. $record->afterFind();
  380. }
  381. return $record;
  382. }
  383. /**
  384. * Returns an array of records populated by incoming data
  385. * @param array $data
  386. * @param bool $callAfterFind
  387. * @param string $index
  388. * @return array - Array of the records
  389. */
  390. public function populateRecords(array $data, $callAfterFind = true, $index = null)
  391. {
  392. $records = array();
  393. foreach($data as $attributes){
  394. if(($record = $this->populateRecord($attributes, $callAfterFind)) !== null){
  395. if($index === null){
  396. $records[] = $record;
  397. }else{
  398. $records[$record->$index] = $record;
  399. }
  400. }
  401. }
  402. return $records;
  403. }
  404. /**********/
  405. /* Events */
  406. /**********/
  407. /**
  408. * @param CEvent $event
  409. */
  410. public function onBeforeSave($event)
  411. {
  412. $this->raiseEvent('onBeforeSave', $event);
  413. }
  414. /**
  415. * @param CEvent $event
  416. */
  417. public function onAfterSave($event)
  418. {
  419. $this->raiseEvent('onAfterSave', $event);
  420. }
  421. /**
  422. * @param CEvent $event
  423. */
  424. public function onBeforeDelete($event)
  425. {
  426. $this->raiseEvent('onBeforeDelete', $event);
  427. }
  428. /**
  429. * @param CEvent $event
  430. */
  431. public function onAfterDelete($event)
  432. {
  433. $this->raiseEvent('onAfterDelete', $event);
  434. }
  435. /**
  436. * @param CEvent $event
  437. */
  438. public function onBeforeFind($event)
  439. {
  440. $this->raiseEvent('onBeforeFind', $event);
  441. }
  442. /**
  443. * @param CEvent $event
  444. */
  445. public function onAfterFind($event)
  446. {
  447. $this->raiseEvent('onAfterFind', $event);
  448. }
  449. /**
  450. * @return bool
  451. */
  452. protected function beforeSave()
  453. {
  454. if($this->hasEventHandler('onBeforeSave')){
  455. $event = new CModelEvent($this);
  456. $this->onBeforeSave($event);
  457. return $event->isValid;
  458. }
  459. return true;
  460. }
  461. protected function afterSave()
  462. {
  463. if($this->hasEventHandler('onAfterSave')){
  464. $this->onAfterSave(new CEvent($this));
  465. }
  466. }
  467. /**
  468. * @return bool
  469. */
  470. protected function beforeDelete()
  471. {
  472. if($this->hasEventHandler('onBeforeDelete')){
  473. $event = new CModelEvent($this);
  474. $this->onBeforeDelete($event);
  475. return $event->isValid;
  476. }
  477. return true;
  478. }
  479. protected function afterDelete()
  480. {
  481. if($this->hasEventHandler('onAfterDelete')){
  482. $this->onAfterDelete(new CEvent($this));
  483. }
  484. }
  485. protected function beforeFind()
  486. {
  487. if($this->hasEventHandler('onBeforeFind')){
  488. $event = new CModelEvent($this);
  489. $this->onBeforeFind($event);
  490. }
  491. }
  492. protected function afterFind()
  493. {
  494. if($this->hasEventHandler('onAfterFind')){
  495. $this->onAfterFind(new CEvent($this));
  496. }
  497. }
  498. /**
  499. * Saves this record
  500. *
  501. * If an attributes specification is sent in it will only validate and save those attributes
  502. *
  503. * @param boolean $runValidation
  504. * @param array $attributes
  505. * @return bool
  506. */
  507. public function save($runValidation = true, $attributes = null){
  508. if(!$runValidation || $this->validate($attributes)){
  509. return $this->getIsNewRecord() ? $this->insert($attributes) : $this->update($attributes);
  510. }
  511. return false;
  512. }
  513. /**
  514. * Saves only a specific subset of attributes as defined by the param
  515. * @param array $attributes
  516. * @return bool
  517. * @throws EMongoException
  518. */
  519. public function saveAttributes($attributes)
  520. {
  521. if($this->getIsNewRecord()){
  522. throw new EMongoException(Yii::t('yii', 'The active record cannot be updated because it is new.'));
  523. }
  524. $this->trace(__FUNCTION__);
  525. $values = array();
  526. foreach($attributes as $name => $value){
  527. if(is_integer($name)){
  528. $v = $this->$value;
  529. if(is_array($this->$value)){
  530. $v = $this->filterRawDocument($this->$value);
  531. }
  532. $values[$value] = $v;
  533. }else{
  534. $values[$name] = $this->$name = $value;
  535. }
  536. }
  537. if(!isset($this->{$this->primaryKey()}) || $this->getPrimaryKey() === null){
  538. throw new EMongoException(Yii::t('yii', 'The active record cannot be updated because its _id is not set!'));
  539. }
  540. return $this->lastError = $this->updateByPk($this->getPrimaryKey(), array('$set' => $values));
  541. }
  542. /**
  543. * Inserts this record
  544. * @param array $attributes
  545. * @return bool
  546. * @throws EMongoException
  547. */
  548. public function insert($attributes = null)
  549. {
  550. if(!$this->getIsNewRecord()){
  551. throw new EMongoException(Yii::t('yii', 'The active record cannot be inserted to database because it is not new.'));
  552. }
  553. if(!$this->beforeSave()){
  554. return false;
  555. }
  556. $this->trace(__FUNCTION__);
  557. if($attributes !== null){
  558. $document = $this->filterRawDocument($this->getAttributes($attributes));
  559. }else{
  560. $document = $this->getRawDocument();
  561. }
  562. if($this->versioned()){
  563. $document[$this->versionField()] = $this->{$this->versionField()} = 1;
  564. }
  565. if(!isset($this->{$this->primaryKey()})){
  566. $document['_id'] = $this->{$this->primaryKey()} = $this->getPrimaryKey();
  567. }
  568. if(YII_DEBUG){
  569. // we're actually physically testing for Yii debug mode here to stop us from
  570. // having to do the serialisation on the update doc normally.
  571. Yii::trace('Executing insert: {$document:' . json_encode($document) . '}', 'extensions.MongoYii.EMongoDocument');
  572. }
  573. if($this->getDbConnection()->enableProfiling){
  574. Yii::beginProfile(
  575. 'extensions.MongoYii.EMongoDocument.query.' . $this->collectionName() . '.insert(' . '{$document:' . json_encode($document) . '})',
  576. 'extensions.MongoYii.EMongoDocument.insert'
  577. );
  578. }
  579. $this->lastError = $this->getCollection()->insert($document, $this->getDbConnection()->getDefaultWriteConcern());
  580. if($this->getDbConnection()->enableProfiling){
  581. Yii::endProfile(
  582. 'extensions.MongoYii.EMongoDocument.query.' . $this->collectionName() . '.insert(' . '{$document:' . json_encode($document) . '})',
  583. 'extensions.MongoYii.EMongoDocument.insert'
  584. );
  585. }
  586. if($this->lastError){
  587. $this->afterSave();
  588. $this->setIsNewRecord(false);
  589. $this->setScenario('update');
  590. return true;
  591. }
  592. return false;
  593. }
  594. /**
  595. * Updates this record
  596. * @param array $attributes
  597. * @return bool
  598. * @throws EMongoException
  599. * @throws EMongoException
  600. */
  601. public function update($attributes = null)
  602. {
  603. if($this->getIsNewRecord()){
  604. throw new EMongoException(Yii::t('yii', 'The active record cannot be updated because it is new.'));
  605. }
  606. if(!$this->beforeSave()){
  607. return false;
  608. }
  609. $this->trace(__FUNCTION__);
  610. if($this->getPrimaryKey() === null){ // An _id is required
  611. throw new EMongoException(Yii::t('yii', 'The active record cannot be updated because it has primary key set.'));
  612. }
  613. $partial = false;
  614. if($attributes !== null){
  615. $attributes = $this->filterRawDocument($this->getAttributes($attributes));
  616. $partial = true;
  617. }elseif($this->getIsPartial()){
  618. foreach($this->_projected_fields as $field => $v){
  619. $attributes[$field] = $this->$field;
  620. }
  621. $attributes = $this->filterRawDocument($attributes);
  622. $partial = true;
  623. }else{
  624. $attributes = $this->getRawDocument();
  625. }
  626. if(isset($attributes['_id'])){
  627. unset($attributes['_id']); // Unset the _id before update
  628. }
  629. if($this->versioned()){
  630. $version = $this->{$this->versionField()};
  631. $attributes[$this->versionField()] = $this->{$this->versionField()} = $this->{$this->versionField()} > 0 ? $this->{$this->versionField()} + 1 : 1;
  632. if($partial === true){ // If this is a partial docuemnt we use $set to replace that partial view
  633. $attributes = array('$set' => $attributes);
  634. if(!isset($this->_projected_fields[$this->versionField()])){
  635. // We cannot rely on a partial document containing the version
  636. // as such it has been disabled for partial documents
  637. throw new EMongoException("You cannot update a versioned partial document unless you project out the version field as well");
  638. }
  639. }
  640. $this->lastError = $this->updateAll(
  641. array($this->primaryKey() => $this->getPrimaryKey(), $this->versionField() => $version),
  642. $attributes,
  643. array('multiple' => false)
  644. );
  645. }else{
  646. if($partial === true){ // If this is a partial docuemnt we use $set to replace that partial view
  647. $attributes = array('$set' => $attributes);
  648. }
  649. $this->lastError = $this->updateByPk($this->getPrimaryKey(), $attributes);
  650. }
  651. if($this->versioned() && $this->lastError['n'] <= 0){
  652. return false;
  653. }
  654. $this->afterSave();
  655. return true;
  656. }
  657. /**
  658. * Deletes this record
  659. * @return bool
  660. * @throws EMongoException
  661. */
  662. public function delete()
  663. {
  664. if($this->getIsNewRecord()){
  665. throw new EMongoException(Yii::t('yii', 'The active record cannot be deleted because it is new.'));
  666. }
  667. $this->trace(__FUNCTION__);
  668. if(!$this->beforeDelete()){
  669. return false;
  670. }
  671. $result = $this->deleteByPk($this->getPrimaryKey());
  672. $this->afterDelete();
  673. return $result;
  674. }
  675. /**
  676. * Checks if a record exists in the database
  677. * @param array $criteria
  678. * @return bool
  679. */
  680. public function exists($criteria = array())
  681. {
  682. $this->trace(__FUNCTION__);
  683. if($criteria instanceof EMongoCriteria){
  684. $criteria = $criteria->getCondition();
  685. }
  686. return $this->getCollection()->findOne($criteria) !== null;
  687. }
  688. /**
  689. * Compares current active record with another one.
  690. * The comparison is made by comparing table name and the primary key values of the two active records.
  691. * @param EMongoDocument $record - record to compare to
  692. * @return boolean - whether the two active records refer to the same row in the database table.
  693. */
  694. public function equals($record)
  695. {
  696. return $this->collectionName() === $record->collectionName() && (string)$this->getPrimaryKey() === (string)$record->getPrimaryKey();
  697. }
  698. /**
  699. * Is basically a find one of the last version to be saved
  700. * @return null|EMongoDocument
  701. */
  702. public function getLatest()
  703. {
  704. $c = $this->find(array($this->primaryKey() => $this->getPrimaryKey()));
  705. if($c->count() <= 0){
  706. return null;
  707. }
  708. foreach($c as $row){
  709. return $this->populateRecord($row);
  710. }
  711. return null;
  712. }
  713. /**
  714. * Find one record
  715. * @param array|EMongoCriteria $criteria
  716. * @param array|string[] $fields
  717. * @return EMongoDocument|null
  718. */
  719. public function findOne($criteria = array(), $fields = array())
  720. {
  721. $this->trace(__FUNCTION__);
  722. $this->beforeFind(); // Apparently this is applied before even scopes...
  723. if($criteria instanceof EMongoCriteria){
  724. $criteria = $criteria->getCondition();
  725. }
  726. $c = $this->getDbCriteria();
  727. $query = $this->mergeCriteria(isset($c['condition']) ? $c['condition'] : array(), $criteria);
  728. $project = $this->mergeCriteria(isset($c['project']) ? $c['project'] : array(), $fields);
  729. Yii::trace(
  730. 'Executing findOne: '.'{$query:' . json_encode($query) . ',$project:' . json_encode($project) . '}',
  731. 'extensions.MongoYii.EMongoDocument'
  732. );
  733. if(
  734. $this->getDbConnection()->queryCachingCount > 0
  735. && $this->getDbConnection()->queryCachingDuration > 0
  736. && $this->getDbConnection()->queryCacheID !== false
  737. && ($cache = Yii::app()->getComponent($this->getDbConnection()->queryCacheID))
  738. && ($cacheKey = 'yii:dbquery'.$this->getDbConnection()->server.':'.$this->getDbConnection()->db .':'.$this->getDbConnection()->getSerialisedQuery($query, $project).':'.$this->getCollection())
  739. && ($result = $cache->get($cacheKey)) !== false
  740. ){
  741. $this->getDbConnection()->queryCachingCount--;
  742. Yii::trace('Query result found in cache', 'extensions.MongoYii.EMongoDocument');
  743. $record = $result[0];
  744. }else{
  745. if($this->getDbConnection()->enableProfiling){
  746. Yii::beginProfile(
  747. 'extensions.MongoYii.EMongoDocument.query.' . $this->collectionName() . '.findOne(' . '{$query:' . json_encode($query) . ',$project:' . json_encode($project) . '}' . ')',
  748. 'extensions.MongoYii.EMongoDocument.findOne'
  749. );
  750. }
  751. $record = $this->getCollection()->findOne($query, $project);
  752. if($this->getDbConnection()->enableProfiling){
  753. Yii::endProfile(
  754. 'extensions.MongoYii.EMongoDocument.query.' . $this->collectionName() . '.findOne(' . '{$query:' . json_encode($query) . ',$project:' . json_encode($project) . '}' . ')',
  755. 'extensions.MongoYii.EMongoDocument.findOne'
  756. );
  757. }
  758. if(isset($cache, $cacheKey)){
  759. $cache->set($cacheKey, array($record), $this->getDbConnection()->queryCachingDuration, $this->getDbConnection()->queryCachingDependency);
  760. }
  761. }
  762. $this->resetScope(false);
  763. if($record === null){
  764. return null;
  765. }
  766. return $this->populateRecord($record, true, $project === array() ? false : true);
  767. }
  768. /**
  769. * Find one record, modifies, and returns it.
  770. * @return null|EMongoDocument
  771. */
  772. public function findAndModify($criteria = array(), $updateDoc = array(), $fields = array(), $options = array())
  773. {
  774. $this->trace(__FUNCTION__);
  775. if ($criteria instanceof EMongoCriteria) {
  776. $criteria = $criteria->getCondition();
  777. }
  778. $c = $this->getDbCriteria();
  779. $criteria = $this->mergeCriteria(isset($c['condition']) ? $c['condition'] : array(), $criteria);
  780. $fields = $this->mergeCriteria(isset($c['project']) ? $c['project'] : array(), $fields);
  781. $options = array_merge($this->getDbConnection()->getDefaultWriteConcern(), $options);
  782. if (YII_DEBUG) {
  783. Yii::trace('Executing findAndModify: { $query: ' . json_encode($criteria) . ', $document: ' . json_encode($updateDoc) . ', $fields: ' . json_encode($fields) . ', $options: ' . json_encode($options) . '}', 'extensions.MongoYii.EMongoDocument');
  784. }
  785. if ($this->getDbConnection()->enableProfiling) {
  786. $token = 'extensions.MongoYii.EMongoDocument.query.' . $this->collectionName() . '.findAndModify({ $query: ' . json_encode($criteria) . ', $document: ' . json_encode($updateDoc) . ', $fields: ' . json_encode($fields) . ', $options: ' . json_encode($options) . '})';
  787. Yii::beginProfile($token, 'extensions.MongoYii.EMongoDocument.findAndModify');
  788. }
  789. $result = $this->getCollection()->findAndModify($criteria, $updateDoc, $fields, $options);
  790. if ($this->getDbConnection()->enableProfiling) {
  791. Yii::enableProfile($token, 'extensions.MongoYii.EMongoDocument.findAndModify');
  792. }
  793. if ($result === null) {
  794. return null;
  795. }
  796. return $this->populateRecord($result, true, $fields === array() ? false : true);
  797. }
  798. /**
  799. * Alias of find
  800. * @param array $criteria
  801. * @param array|string[] $fields
  802. * @return EMongoCursor|EMongoDocument[]
  803. */
  804. public function findAll($criteria = array(), $fields = array())
  805. {
  806. return $this->find($criteria, $fields);
  807. }
  808. /**
  809. * Alias of find
  810. * @param array $criteria
  811. * @param array|string[] $fields
  812. * @return EMongoCursor|EMongoDocument[]
  813. */
  814. public function findAllByAttributes($criteria = array(), $fields = array())
  815. {
  816. return $this->find($criteria, $fields);
  817. }
  818. /**
  819. * Finds all records based on $pk
  820. * @param mixed $pk - String, MongoID or array of strings or MongoID values (one can mix strings and MongoID in the array)
  821. * @param array|string[] $fields
  822. * @return EMongoCursor|EMongoDocument[]
  823. * @throws EMongoException
  824. */
  825. public function findAllByPk($pk, $fields = array())
  826. {
  827. if(is_string($pk) || $pk instanceof MongoId){
  828. return $this->find(array($this->primaryKey() => $this->getPrimaryKey($pk)), $fields);
  829. }
  830. if(!is_array($pk)){
  831. throw new EMongoException(Yii::t('yii', 'Set an incorrect primary key.'));
  832. }
  833. foreach($pk as $key => $value){
  834. $pk[$key] = $this->getPrimaryKey($value);
  835. }
  836. return $this->find(array($this->primaryKey() => array('$in' => $pk)), $fields);
  837. }
  838. /**
  839. * Find some records
  840. * @param array|EMongoCriteria $criteria
  841. * @param array|string[] $fields
  842. * @return EMongoCursor|EMongoDocument[]
  843. */
  844. public function find($criteria = array(), $fields = array())
  845. {
  846. $this->trace(__FUNCTION__);
  847. $this->beforeFind(); // Apparently this is applied before even scopes...
  848. if($criteria instanceof EMongoCriteria){
  849. $c = $criteria->mergeWith($this->getDbCriteria())->toArray();
  850. $criteria = array();
  851. }else{
  852. $c = $this->getDbCriteria();
  853. }
  854. $query = $this->mergeCriteria(isset($c['condition']) ? $c['condition'] : array(), $criteria);
  855. $project = $this->mergeCriteria(isset($c['project']) ? $c['project'] : array(), $fields);
  856. Yii::trace(
  857. 'Executing find: ' . '{$query:'.json_encode($query) . ',$project:'.json_encode($project)
  858. . (isset($c['sort']) ? ',$sort:'.json_encode($c['sort']).',' : '')
  859. . (isset($c['skip']) ? ',$skip:'.json_encode($c['skip']).',' : '')
  860. . (isset($c['limit']) ? ',$limit:'.json_encode($c['limit']).',' : '')
  861. .'}',
  862. 'extensions.MongoYii.EMongoDocument'
  863. );
  864. if($this->getDbConnection()->enableProfiling){
  865. $token = 'extensions.MongoYii.EMongoDocument.query.' . $this->collectionName() . '.find(' . '{$query:' . json_encode($query)
  866. . ',$project:'.json_encode($project)
  867. . (isset($c['sort']) ? ',$sort:'.json_encode($c['sort']).',' : '')
  868. . (isset($c['skip']) ? ',$skip:'.json_encode($c['skip']).',' : '')
  869. . (isset($c['limit']) ? ',$limit:'.json_encode($c['limit']).',' : '')
  870. .'})';
  871. Yii::beginProfile($token, 'extensions.MongoYii.EMongoDocument.find');
  872. }
  873. if($c !== array()){
  874. $cursor = new EMongoCursor($this, $query, $project);
  875. if(isset($c['sort'])){
  876. $cursor->sort($c['sort']);
  877. }
  878. if(isset($c['skip'])){
  879. $cursor->skip($c['skip']);
  880. }
  881. if(isset($c['limit'])){
  882. $cursor->limit($c['limit']);
  883. }
  884. $this->resetScope(false);
  885. }else{
  886. $cursor = new EMongoCursor($this, $criteria, $project);
  887. }
  888. if($this->getDbConnection()->enableProfiling){
  889. Yii::endProfile($token, 'extensions.MongoYii.EMongoDocument.find');
  890. }
  891. return $cursor;
  892. }
  893. /**
  894. * Finds one by _id
  895. * @param string|MongoId $_id
  896. * @param array|string[] $fields
  897. * @return EMongoDocument|null
  898. */
  899. public function findBy_id($_id, $fields = array())
  900. {
  901. $this->trace(__FUNCTION__);
  902. $_id = $this->getPrimaryKey($_id);
  903. return $this->findOne(array($this->primaryKey() => $_id), $fields);
  904. }
  905. /**
  906. * An alias for findBy_id() that relates to Yiis own findByPk
  907. * @param string|MongoId $pk
  908. * @param array|string[] $fields
  909. * @return EMongoDocument|null
  910. */
  911. public function findByPk($pk, $fields = array())
  912. {
  913. $this->trace(__FUNCTION__);
  914. return $this->findBy_id($pk, $fields);
  915. }
  916. /**
  917. * Delete record by pk
  918. * @param string|MongoId $pk
  919. * @param array|EMongoCriteria $criteria
  920. * @param array $options
  921. * @return mixed
  922. */
  923. public function deleteByPk($pk, $criteria = array(), $options = array())
  924. {
  925. $this->trace(__FUNCTION__);
  926. if($criteria instanceof EMongoCriteria){
  927. $criteria = $criteria->getCondition();
  928. }
  929. $pk = $this->getPrimaryKey($pk);
  930. $query=array_merge(array($this->primaryKey() => $pk), $criteria);
  931. Yii::trace('Executing deleteByPk: ' . '{$query:' . json_encode($query) . '}', 'extensions.MongoYii.EMongoDocument');
  932. if($this->getDbConnection()->enableProfiling){
  933. Yii::beginProfile(
  934. 'extensions.MongoYii.EMongoDocument.query.' . $this->collectionName() . '.deleteByPk({$query:' . json_encode($query) . '})',
  935. 'extensions.MongoYii.EMongoDocument.deleteByPk'
  936. );
  937. }
  938. $result = $this->getCollection()->remove($query, array_merge($this->getDbConnection()->getDefaultWriteConcern(), $options));
  939. if($this->getDbConnection()->enableProfiling){
  940. Yii::endProfile(
  941. 'extensions.MongoYii.EMongoDocument.query.' . $this->collectionName() . '.deleteByPk({$query:' . json_encode($query) . '})',
  942. 'extensions.MongoYii.EMongoDocument.deleteByPk'
  943. );
  944. }
  945. return $result;
  946. }
  947. /**
  948. * Update record by PK
  949. * @param string|MongoId $pk
  950. * @param array $updateDoc
  951. * @param array|EMongoCriteria $criteria
  952. * @param array $options
  953. * @return bool
  954. */
  955. public function updateByPk($pk, $updateDoc = array(), $criteria = array(), $options = array())
  956. {
  957. $this->trace(__FUNCTION__);
  958. if($criteria instanceof EMongoCriteria){
  959. $criteria = $criteria->getCondition();
  960. }
  961. $pk = $this->getPrimaryKey($pk);
  962. $query=$this->mergeCriteria($criteria, array($this->primaryKey() => $pk));
  963. if(YII_DEBUG){
  964. // we're actually physically testing for Yii debug mode here to stop us from
  965. // having to do the serialisation on the update doc normally.
  966. Yii::trace(
  967. 'Executing updateByPk: {$query:' . json_encode($query) . ',$document:' . json_encode($updateDoc) . '}',
  968. 'extensions.MongoYii.EMongoDocument'
  969. );
  970. }
  971. if($this->getDbConnection()->enableProfiling){
  972. Yii::beginProfile(
  973. 'extensions.MongoYii.EMongoDocument.query.' . $this->collectionName() . '.updateByPk({$query:' . json_encode($query) . ',$document:' . json_encode($updateDoc) . '})',
  974. 'extensions.MongoYii.EMongoDocument.updateByPk'
  975. );
  976. }
  977. $result = $this->getCollection()->update(
  978. $query,
  979. $updateDoc,
  980. array_merge($this->getDbConnection()->getDefaultWriteConcern(), $options)
  981. );
  982. if($this->getDbConnection()->enableProfiling){
  983. Yii::endProfile(
  984. 'extensions.MongoYii.EMongoDocument.query.' . $this->collectionName() . '.updateByPk({$query:' . json_encode($query) . ',$document:' . json_encode($updateDoc) . '})',
  985. 'extensions.MongoYii.EMongoDocument.updateByPk'
  986. );
  987. }
  988. return $result;
  989. }
  990. /**
  991. * Update all records matching a criteria
  992. * @param array|EMongoCriteria $criteria
  993. * @param array $updateDoc
  994. * @param array $options
  995. * @return bool
  996. */
  997. public function updateAll($criteria = array(), $updateDoc = array(), $options = array('multiple' => true))
  998. {
  999. $this->trace(__FUNCTION__);
  1000. if($criteria instanceof EMongoCriteria){
  1001. $criteria = $criteria->getCondition();
  1002. }
  1003. $options = array_merge($this->getDbConnection()->getDefaultWriteConcern(), $options);
  1004. if(YII_DEBUG){
  1005. // we're actually physically testing for Yii debug mode here to stop us from
  1006. // having to do the serialisation on the update doc normally.
  1007. Yii::trace(
  1008. 'Executing updateAll: {$query:' . json_encode($criteria)
  1009. . ',$document:' . json_encode($updateDoc) . ',$options:' . json_encode($options) . '}',
  1010. 'extensions.MongoYii.EMongoDocument'
  1011. );
  1012. }
  1013. if($this->getDbConnection()->enableProfiling){
  1014. $token = 'extensions.MongoYii.EMongoDocument.query.' . $this->collectionName() . '.updateAll({$query:' . json_encode($criteria)
  1015. . ',$document:'.json_encode($updateDoc)
  1016. . ',$options:'.json_encode($options).'})';
  1017. Yii::beginProfile($token, 'extensions.MongoYii.EMongoDocument.updateAll');
  1018. }
  1019. $result = $this->getCollection()->update($criteria, $updateDoc, $options);
  1020. if($this->getDbConnection()->enableProfiling){
  1021. Yii::endProfile($token, 'extensions.MongoYii.EMongoDocument.updateAll');
  1022. }
  1023. return $result;
  1024. }
  1025. /**
  1026. * Delete all records matching a criteria
  1027. * @param array|EMongoCriteria $criteria
  1028. * @param array $options
  1029. * @return mixed
  1030. */
  1031. public function deleteAll($criteria = array(), $options = array())
  1032. {
  1033. $this->trace(__FUNCTION__);
  1034. if($criteria instanceof EMongoCriteria){
  1035. $criteria = $criteria->getCondition();
  1036. }
  1037. Yii::trace(
  1038. 'Executing deleteAll: {$query:' . json_encode($criteria) . '}',
  1039. 'extensions.MongoYii.EMongoDocument'
  1040. );
  1041. if($this->getDbConnection()->enableProfiling){
  1042. Yii::beginProfile(
  1043. 'extensions.MongoYii.EMongoDocument.query.' . $this->collectionName() . '.deleteAll({$query:' . json_encode($criteria) . '})',
  1044. 'extensions.MongoYii.EMongoDocument.deleteAll'
  1045. );
  1046. }
  1047. $result = $this->getCollection()->remove($criteria, array_merge($this->getDbConnection()->getDefaultWriteConcern(), $options));
  1048. if($this->getDbConnection()->enableProfiling){
  1049. Yii::endProfile(
  1050. 'extensions.MongoYii.EMongoDocument.query.' . $this->collectionName() . '.deleteAll({$query:' . json_encode($criteria) . '})',
  1051. 'extensions.MongoYii.EMongoDocument.deleteAll'
  1052. );
  1053. }
  1054. return $result;
  1055. }
  1056. /**
  1057. * @see http://www.yiiframework.com/doc/api/1.1/CActiveRecord#saveCounters-detail
  1058. * @param array $counters
  1059. * @param null $lower - define a lower that the counter should not pass. IS NOT ATOMIC
  1060. * @param null $upper
  1061. * @return bool
  1062. * @throws EMongoException
  1063. */
  1064. public function saveCounters(array $counters, $lower = null, $upper = null)
  1065. {
  1066. $this->trace(__FUNCTION__);
  1067. if($this->getIsNewRecord()){
  1068. throw new EMongoException(Yii::t('yii', 'The active record cannot be updated because it is new.'));
  1069. }
  1070. if(sizeof($counters) > 0){
  1071. foreach($counters as $key => $value){
  1072. if(
  1073. ($lower !== null && (($this->$key + $value) >= $lower)) ||
  1074. ($upper !== null && (($this->$key + $value) <= $upper)) ||
  1075. ($lower === null && $upper === null)
  1076. ){
  1077. $this->$key = $this->$key + $value;
  1078. }else{
  1079. unset($counters[$key]);
  1080. }
  1081. }
  1082. if(count($counters) > 0){
  1083. return $this->updateByPk($this->getPrimaryKey(), array('$inc' => $counters));
  1084. }
  1085. }
  1086. return true; // Assume true since the action did run it just had nothing to update...
  1087. }
  1088. /**
  1089. * Count() allows you to count all the documents returned by a certain condition, it is analogous
  1090. * to $db->collection->find()->count() and basically does exactly that...
  1091. * @param EMongoCriteria|array $criteria
  1092. * @return int
  1093. */
  1094. public function count($criteria = array())
  1095. {
  1096. $this->trace(__FUNCTION__);
  1097. // If we provide a manual criteria via EMongoCriteria or an array we do not use the models own DbCriteria
  1098. if (is_array($criteria) && empty($criteria)){
  1099. $criteria = $this->getDbCriteria();
  1100. $criteria = (isset($criteria['condition']) ? $criteria['condition'] : array());
  1101. }
  1102. if($criteria instanceof EMongoCriteria){
  1103. $criteria = $criteria->getCondition();
  1104. }
  1105. return $this->getCollection()->find($criteria)->count();
  1106. }
  1107. /**
  1108. * Gives basic searching abilities for things like CGridView
  1109. * @param array $query - allows you to specify a query which should always take hold along with the searched fields
  1110. * @param array $project - search fields
  1111. * @param bool $partialMatch
  1112. * @param array $sort
  1113. * @return EMongoDataProvider
  1114. */
  1115. public function search($query = array(), $project = array(), $partialMatch=false, $sort = array())
  1116. {
  1117. $this->trace(__FUNCTION__);
  1118. foreach($this->getSafeAttributeNames() as $attribute){
  1119. $value = $this->{$attribute};
  1120. if($value !== null && $value !== ''){
  1121. if((is_array($value) && count($value)) || is_object($value) || is_bool($value)){
  1122. $query[$attribute] = $value;
  1123. }elseif(preg_match('/^(?:\s*(<>|<=|>=|<|>|=))?(.*)$/', $value, $matches)){
  1124. $value = $matches[2];
  1125. $op = $matches[1];
  1126. if($partialMatch === true){
  1127. $value = new MongoRegex("/$value/i");
  1128. }else{
  1129. if(
  1130. !is_bool($value) && !is_array($value) && preg_match('/^([0-9]|[1-9]{1}\d+)$/' /* Will only match real integers, unsigned */, $value) > 0
  1131. && (
  1132. (PHP_INT_MAX > 2147483647 && (string)$value < '9223372036854775807') /* If it is a 64 bit system and the value is under the long max */
  1133. || (string)$value < '2147483647' /* value is under 32bit limit */
  1134. )
  1135. ){
  1136. $value = (int)$value;
  1137. }
  1138. }
  1139. switch($op){
  1140. case '<>':
  1141. $query[$attribute] = array('$ne' => $value);
  1142. break;
  1143. case '<=':
  1144. $query[$attribute] = array('$lte' => $value);
  1145. break;
  1146. case '>=':
  1147. $query[$attribute] = array('$gte' => $value);
  1148. break;
  1149. case '<':
  1150. $query[$attribute] = array('$lt' => $value);
  1151. break;
  1152. case '>':
  1153. $query[$attribute] = array('$gt' => $value);
  1154. break;
  1155. case '=':
  1156. default:
  1157. $query[$attribute] = $value;
  1158. break;
  1159. }
  1160. }
  1161. }
  1162. }
  1163. return new EMongoDataProvider($this, array('criteria' => array('condition' => $query, 'project' => $project, 'sort' => $sort)));
  1164. }
  1165. /**
  1166. * This is an aggregate helper on the model
  1167. * Note: This does not return the model but instead the result array directly from MongoDB.
  1168. * @param array $pipeline
  1169. * @return mixed
  1170. */
  1171. public function aggregate($pipeline)
  1172. {
  1173. $this->trace(__FUNCTION__);
  1174. return $this->getDbConnection()->aggregate($this->collectionName(), $pipeline);
  1175. }
  1176. /**
  1177. * A distinct helper on the model, this is not the same as the aggregation framework
  1178. * distinct
  1179. * @link http://docs.mongodb.org/manual/reference/command/distinct/
  1180. * @param string $key
  1181. * @param array $query
  1182. * @return mixed
  1183. */
  1184. public function distinct($key, $query = array())
  1185. {
  1186. $this->trace(__FUNCTION__);
  1187. $c = $this->getDbCriteria();
  1188. if(is_array($c) && isset($c['condition']) && !empty($c['condition'])){
  1189. $query = CMap::mergeArray($query, $c['condition']);
  1190. }
  1191. return Yii::app()->mongodb->command(array(
  1192. 'distinct' => $this->collectionName(),
  1193. 'key' => $key,
  1194. 'query' => $query ? $query : null
  1195. ));
  1196. }
  1197. /**
  1198. * A mapreduce helper for this model
  1199. * @param MongoCode $map
  1200. * @param MongoCode $reduce
  1201. * @param MongoCode $finalize
  1202. * @param array $out
  1203. * @param array $query
  1204. * @param array $options // All other options for input to the command
  1205. * @return mixed
  1206. */
  1207. public function mapreduce($map, $reduce, $finalize = null, $out = array(), $query = array(), $options = array())
  1208. {
  1209. return $this->getDbConnection()->getDB()->command(array_merge(array(
  1210. 'mapreduce' => $this->collectionName(),
  1211. 'map' => $map,
  1212. 'reduce' => $reduce,
  1213. 'finalize' => $finalize,
  1214. 'query' => $query,
  1215. 'out' => $out
  1216. ),$options));
  1217. }
  1218. /**
  1219. * Allows a user to ensure a number of indexes using the format:
  1220. *
  1221. * ensureIndexes(array(
  1222. * array(array('email' => 1), array('unique' => true))
  1223. * ))
  1224. *
  1225. * where the 0 offset in each nested array is the fields for the index and the 1 offset
  1226. * is the options. You don't have to define options for the index.
  1227. *
  1228. * or:
  1229. *
  1230. * ensureIndexes(array(
  1231. * array('email' => 1, 'address' => -1)
  1232. * ))
  1233. *
  1234. * @param unknown $indexes
  1235. */
  1236. public function ensureIndexes($indexes)
  1237. {
  1238. foreach($indexes as $index){
  1239. if(isset($index[0])){
  1240. $this->getCollection()->ensureIndex($index[0], isset($index[1]) ? $index[1] : array());
  1241. }else{
  1242. $this->getCollection()->ensureIndex($index, array());
  1243. }
  1244. }
  1245. return true;
  1246. }
  1247. /**
  1248. * Sets the parameters for query caching.
  1249. * This is a shortcut method to {@link CDbConnection::cache()}.
  1250. * It changes the query caching parameter of the {@link dbConnection} instance.
  1251. * @param integer $duration the number of seconds that query results may remain valid in cache.
  1252. * If this is 0, the caching will be disabled.
  1253. * @param CCacheDependency|ICacheDependency $dependency the dependency that will be used when saving
  1254. * the query results into cache.
  1255. * @param integer $queryCount number of MongoDB queries that need to be cached after calling this method. Defaults to 1,
  1256. * meaning that the next MongoDB query will be cached.
  1257. * @return EMongoDocument the active record instance itself.
  1258. */
  1259. public function cache($duration, $dependency=null, $queryCount=1)
  1260. {
  1261. $this->getDbConnection()->cache($duration, $dependency, $queryCount);
  1262. return $this;
  1263. }
  1264. /**
  1265. * Refreshes the data from the database
  1266. * @return bool
  1267. */
  1268. public function refresh()
  1269. {
  1270. $this->trace(__FUNCTION__);
  1271. if(
  1272. !$this->getIsNewRecord() &&
  1273. ($record = $this->getCollection()->findOne(array($this->primaryKey() => $this->getPrimaryKey()))) !== null
  1274. ){
  1275. $this->clean();
  1276. foreach($record as $name => $column){
  1277. $this->$name = $record[$name];
  1278. }
  1279. return true;
  1280. }
  1281. return false;
  1282. }
  1283. /**
  1284. * A bit deceptive, this actually gets the last response from either save() or update(). The reason it is called this
  1285. * is because MongoDB calls it this and so it seems better to have unity on that front.
  1286. * @return array
  1287. */
  1288. public function getLastError()
  1289. {
  1290. return $this->lastError;
  1291. }
  1292. /**
  1293. * gets and if null sets the db criteria for this model
  1294. * @param bool $createIfNull
  1295. * @return array
  1296. */
  1297. public function getDbCriteria($createIfNull = true)
  1298. {
  1299. if($this->_criteria === null){
  1300. if(($c = $this->defaultScope()) !== array() || $createIfNull){
  1301. $this->_criteria = $c;
  1302. }else{
  1303. return array();
  1304. }
  1305. }
  1306. return $this->_criteria;
  1307. }
  1308. /**
  1309. * Sets the db criteria for this model
  1310. * @param array $criteria
  1311. * @return array
  1312. */
  1313. public function setDbCriteria(array $criteria)
  1314. {
  1315. return $this->_criteria = $criteria;
  1316. }
  1317. /**
  1318. * Merges the current DB Criteria with the inputted one
  1319. * @param array|EMongoCriteria $newCriteria
  1320. * @return array
  1321. */
  1322. public function mergeDbCriteria($newCriteria)
  1323. {
  1324. if($newCriteria instanceof EMongoCriteria){
  1325. $newCriteria = $newCriteria->toArray();
  1326. }
  1327. return $this->setDbCriteria($this->mergeCriteria($this->getDbCriteria(), $newCriteria));
  1328. }
  1329. /**
  1330. * Gets the collection for this model
  1331. * @return MongoCollection
  1332. */
  1333. public function getCollection()
  1334. {
  1335. return $this->getDbConnection()->{$this->collectionName()};
  1336. }
  1337. /**
  1338. * Merges two criteria objects. Best used for scopes
  1339. * @param array $oldCriteria
  1340. * @param array $newCriteria
  1341. * @return array
  1342. */
  1343. public function mergeCriteria($oldCriteria, $newCriteria)
  1344. {
  1345. return CMap::mergeArray($oldCriteria, $newCriteria);
  1346. }
  1347. /**
  1348. * Produces a trace message for functions in this class
  1349. * @param string $func
  1350. */
  1351. public function trace($func)
  1352. {
  1353. Yii::trace(get_class($this) . '.' . $func . '()', 'extensions.MongoYii.EMongoDocument');
  1354. }
  1355. }