EMongoModel.php 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722
  1. <?php
  2. class EMongoModel extends CModel
  3. {
  4. /**
  5. * @var EMongoClient the default database connection for all active record classes.
  6. * By default, this is the 'mongodb' application component.
  7. * @see getDbConnection
  8. */
  9. public static $db;
  10. private $_errors = array(); // attribute name => array of errors
  11. private $_attributes = array();
  12. private $_related = array();
  13. private $_partial = false;
  14. /**
  15. * @see yii/framework/CComponent::__get()
  16. * @param string $name
  17. * @return mixed
  18. */
  19. public function __get($name)
  20. {
  21. if(isset($this->_attributes[$name])){
  22. return $this->_attributes[$name];
  23. }
  24. if(isset($this->_related[$name])){
  25. return $this->_related[$name];
  26. }
  27. if(array_key_exists($name, $this->relations())){
  28. return $this->_related[$name] = $this->getRelated($name);
  29. }
  30. try{
  31. return parent::__get($name);
  32. }catch(CException $e){
  33. $getter = 'get' . $name;
  34. if(method_exists($this, $getter)){
  35. throw $e;
  36. }elseif(strncasecmp($name, 'on', 2) === 0 && method_exists($this, $name)){
  37. throw $e;
  38. }
  39. return null;
  40. }
  41. }
  42. /**
  43. * @see CComponent::__set()
  44. * @param string $name
  45. * @param mixed $value
  46. * @return mixed
  47. */
  48. public function __set($name, $value)
  49. {
  50. if(isset($this->_related[$name]) || array_key_exists($name, $this->relations())){
  51. return $this->_related[$name] = $value;
  52. }
  53. // This might be a little unperformant actually since Yiis own active record detects
  54. // If an attribute can be set first to ensure speed of accessing local variables...hmmm
  55. try{
  56. return parent::__set($name, $value);
  57. }catch(CException $e){
  58. return $this->setAttribute($name, $value);
  59. }
  60. }
  61. /**
  62. * @see CComponent::__isset()
  63. * @param string $name
  64. * @return bool
  65. */
  66. public function __isset($name)
  67. {
  68. if(isset($this->_attributes[$name])){
  69. return true;
  70. }
  71. if(isset($this->_related[$name])){
  72. return true;
  73. }
  74. if(array_key_exists($name, $this->relations())){
  75. return $this->getRelated($name) !== null;
  76. }
  77. return parent::__isset($name);
  78. }
  79. /**
  80. * @see CComponent::__unset()
  81. * @param string $name
  82. * @return void
  83. */
  84. public function __unset($name)
  85. {
  86. if(isset($this->_attributes[$name])){
  87. unset($this->_attributes[$name]);
  88. }elseif(isset($this->_related[$name])){
  89. unset($this->_related[$name]);
  90. }else{
  91. parent::__unset($name);
  92. }
  93. }
  94. /**
  95. * @see CComponent::__call()
  96. * @param string $name
  97. * @param array $parameters
  98. * @return mixed
  99. */
  100. public function __call($name,$parameters)
  101. {
  102. if(!array_key_exists($name, $this->relations())){
  103. return parent::__call($name,$parameters);
  104. }
  105. if(empty($parameters)){
  106. return $this->getRelated($name, false);
  107. }
  108. return $this->getRelated($name, false, $parameters[0]);
  109. }
  110. /**
  111. * This sets up our model.
  112. * Apart from what Yii normally does this also sets a field cache for reflection so that we only ever do reflection once to
  113. * understand what fields are in our model.
  114. * @param string $scenario
  115. */
  116. public function __construct($scenario = 'insert')
  117. {
  118. $this->getDbConnection()->setDocumentCache($this);
  119. if($scenario === null){ // internally used by populateRecord() and model()
  120. return;
  121. }
  122. $this->setScenario($scenario);
  123. $this->init();
  124. $this->attachBehaviors($this->behaviors());
  125. $this->afterConstruct();
  126. }
  127. /**
  128. * Initializes this model.
  129. * This method is invoked when an AR instance is newly created and has
  130. * its {@link scenario} set.
  131. * You may override this method to provide code that is needed to initialize the model (e.g. setting
  132. * initial property values.)
  133. * @return bool
  134. */
  135. public function init()
  136. {
  137. return true;
  138. }
  139. /**
  140. * @see CModel::attributeNames()
  141. * @return array
  142. */
  143. public function attributeNames()
  144. {
  145. $fields = $this->getDbConnection()->getFieldCache(get_class($this), true);
  146. $cols = array_merge($fields, array_keys($this->_attributes));
  147. return $cols !== null ? $cols : array();
  148. }
  149. /**
  150. * Holds all our relations
  151. * @return array
  152. */
  153. public function relations()
  154. {
  155. return array();
  156. }
  157. /**
  158. * Finds out if a document attributes actually exists
  159. * @param string $name
  160. * @return bool
  161. */
  162. public function hasAttribute($name)
  163. {
  164. $attrs = $this->_attributes;
  165. $fields = $this->getDbConnection()->getFieldCache(get_class($this));
  166. return isset($attrs[$name]) || isset($fields[$name]) || property_exists($this, $name) ? true : false;
  167. }
  168. /**
  169. * Sets the attribute of the model
  170. * @param string $name
  171. * @param mixed $value
  172. * @return bool
  173. */
  174. public function setAttribute($name, $value)
  175. {
  176. if(property_exists($this,$name)){
  177. $this->$name = $value;
  178. }else{
  179. $this->_attributes[$name] = $value;
  180. }
  181. return true;
  182. }
  183. /**
  184. * Gets a document attribute
  185. * @param string $name
  186. * @return mixed
  187. */
  188. public function getAttribute($name)
  189. {
  190. if(property_exists($this, $name)){
  191. return $this->$name;
  192. }
  193. if(isset($this->_attributes[$name])){
  194. return $this->_attributes[$name];
  195. }
  196. return null;
  197. }
  198. /**
  199. * @see CModel::getAttributes()
  200. * @param bool $names
  201. * @return array
  202. */
  203. public function getAttributes($names = true)
  204. {
  205. $attributes = $this->_attributes;
  206. $fields = $this->getDbConnection()->getFieldCache(get_class($this));
  207. if(is_array($fields)){
  208. foreach($fields as $name){
  209. $attributes[$name] = $this->$name;
  210. }
  211. }
  212. if(!is_array($names)){
  213. return $attributes;
  214. }
  215. $attrs = array();
  216. foreach($names as $name){
  217. if(property_exists($this, $name)){
  218. $attrs[$name] = $this->$name;
  219. }else{
  220. $attrs[$name] = isset($attributes[$name]) ? $attributes[$name] : null;
  221. }
  222. }
  223. return $attrs;
  224. }
  225. /**
  226. * Sets the attribute values in a massive way.
  227. * @param array $values attribute values (name=>value) to be set.
  228. * @param boolean $safeOnly whether the assignments should only be done to the safe attributes.
  229. * A safe attribute is one that is associated with a validation rule in the current {@link scenario}.
  230. * @see getSafeAttributeNames
  231. * @see attributeNames
  232. */
  233. public function setAttributes($values, $safeOnly = true)
  234. {
  235. if(!is_array($values)){
  236. return;
  237. }
  238. $attributes = array_flip($safeOnly ? $this->getSafeAttributeNames() : $this->attributeNames());
  239. $_meta = $this->getDbConnection()->getDocumentCache(get_class($this));
  240. foreach($values as $name => $value){
  241. $field_meta = isset($_meta[$name]) ? $_meta[$name] : array();
  242. if($safeOnly){
  243. if(isset($attributes[$name])){
  244. $this->$name = !is_bool($value) && !is_array($value) && !is_object($value) && preg_match('/^([0-9]|[1-9]{1}\d+)$/' /* Will only match real integers, unsigned */, $value) > 0
  245. && ( (PHP_INT_MAX > 2147483647 && (string)$value < '9223372036854775807') /* If it is a 64 bit system and the value is under the long max */
  246. || (string)$value < '2147483647' /* value is under 32bit limit */) ? (int)$value : $value;
  247. }elseif($safeOnly){
  248. $this->onUnsafeAttribute($name, $value);
  249. }
  250. }else{
  251. $this->$name = !is_bool($value) && !is_array($value) && !is_object($value) && preg_match('/^([0-9]|[1-9]{1}\d+)$$/' /* Will only match real integers, unsigned */, $value) > 0
  252. && ( (PHP_INT_MAX > 2147483647 && (string)$value < '9223372036854775807') || (string)$value < '2147483647') ? (int)$value : $value;
  253. }
  254. }
  255. }
  256. /**
  257. * Sets the attributes to be null.
  258. * @param array $names list of attributes to be set null. If this parameter is not given,
  259. * all attributes as specified by {@link attributeNames} will have their values unset.
  260. * @since 1.1.3
  261. */
  262. public function unsetAttributes($names = null)
  263. {
  264. if($names === null){
  265. $names = $this->attributeNames();
  266. }
  267. foreach($names as $name){
  268. $this->$name = null;
  269. }
  270. }
  271. /**
  272. * Allows for mass assignment of the record in question
  273. */
  274. public function populateRecord($attributes, $runEvent = true)
  275. {
  276. if($attributes === false || $attributes === null){
  277. return null;
  278. }
  279. $record = new $this;
  280. $record->setScenario('update');
  281. foreach($attributes as $name => $value){
  282. $record->setAttribute($name, $value);
  283. }
  284. $record->init();
  285. $record->attachBehaviors($record->behaviors());
  286. if($runEvent){
  287. $record->afterConstruct();
  288. }
  289. return $record;
  290. }
  291. /**
  292. * Sets whether or not this is a partial document
  293. * @param $partial
  294. */
  295. public function setIsPartial($partial)
  296. {
  297. $this->_partial = $partial;
  298. }
  299. /**
  300. * Gets whether or not this is a partial document, i.e. it only has some
  301. * of its fields present
  302. */
  303. public function getIsPartial()
  304. {
  305. return $this->_partial;
  306. }
  307. /**
  308. * You can change the primarykey but due to how MongoDB
  309. * actually works this IS NOT RECOMMENDED
  310. */
  311. public function primaryKey()
  312. {
  313. return '_id';
  314. }
  315. /**
  316. * Returns the related record(s).
  317. * This method will return the related record(s) of the current record.
  318. * If the relation is 'one' it will return a single object
  319. * or null if the object does not exist.
  320. * If the relation is 'many' it will return an array of objects
  321. * or an empty iterator.
  322. * @param string $name the relation name (see {@link relations})
  323. * @param boolean $refresh whether to reload the related objects from database. Defaults to false.
  324. * @param mixed $params array with additional parameters that customize the query conditions as specified in the relation declaration.
  325. * @return mixed the related object(s).
  326. * @throws EMongoException if the relation is not specified in {@link relations}.
  327. */
  328. public function getRelated($name, $refresh = false, $params = array())
  329. {
  330. if(!$refresh && $params === array() && (isset($this->_related[$name]) || array_key_exists($name, $this->_related))){
  331. return $this->_related[$name];
  332. }
  333. $relations = $this->relations();
  334. if(!isset($relations[$name])){
  335. throw new EMongoException(
  336. Yii::t(
  337. 'yii',
  338. '{class} does not have relation "{name}".',
  339. array('{class}' => get_class($this), '{name}' => $name)
  340. )
  341. );
  342. }
  343. Yii::trace('lazy loading ' . get_class($this) . '.' . $name, 'extensions.MongoYii.EMongoModel');
  344. $cursor = array();
  345. $relation = $relations[$name];
  346. // Let's get the parts of the relation to understand it entirety of its context
  347. $cname = $relation[1];
  348. $fkey = $relation[2];
  349. $pk = isset($relation['on']) ? $this->{$relation['on']} : $this->getPrimaryKey();
  350. $pkName = isset($relation['on']) ? $relation['on'] : $this->primaryKey();
  351. // This will detect . notation key names like AuthorName.id
  352. if(strpos($pkName, '.') !== false){
  353. $pk = array();
  354. $parts = explode('.', $pkName);
  355. if($this->hasAttribute($parts[0])){
  356. $val = $this->{$parts[0]};
  357. if(!is_array($val) && !is_object($val)){
  358. // continue
  359. }elseif(is_object($val) && property_exists($val, $parts[1])){
  360. $pk[] = $val->{$parts[1]};
  361. }elseif(is_array($val) && isset($val[$parts[1]])){
  362. $pk[] = $val[$parts[1]];
  363. }else{
  364. foreach($val as $k => $v){
  365. if(is_array($v) && isset($v[$parts[1]])){
  366. $pk[] = $v[$parts[1]];
  367. }elseif(is_object($v) && property_exists($v, $parts[1])){
  368. $pk[] = $v->{$parts[1]};
  369. }
  370. }
  371. }
  372. }
  373. }
  374. // This takes care of cases where the PK is an DBRef and only one DBRef, where it could
  375. // be mistaken as a multikey field
  376. if($relation[0] === 'one' && is_array($pk) && array_key_exists('$ref', $pk)){
  377. $pk = array($pk);
  378. }
  379. // Form the where clause
  380. $where = $params;
  381. if(isset($relation['where']) && !$params){
  382. $where = array_merge($relation['where'], $params);
  383. }
  384. // Find out what the pk is and what kind of condition I should apply to it
  385. if(is_array($pk)){
  386. //It is an array of references
  387. if(MongoDBRef::isRef(reset($pk))){
  388. $result = array();
  389. foreach($pk as $singleReference){
  390. $row = $this->populateReference($singleReference, $cname);
  391. // When $row does not exists it will return null. It will not add it to $result
  392. array_push($result, $row);
  393. }
  394. // When $row is null count($result) will be 0 and $result will be an empty array
  395. // Because we are a one relation we want to return null when a row does not exists
  396. // Currently it was returning an empty array
  397. if($relation[0] === 'one' && count($result) > 0){
  398. $result = $result[0];
  399. }
  400. return $this->_related[$name] = $result;
  401. }
  402. // It is an array of _ids
  403. $clause = array_merge($where, array($fkey => array('$in' => $pk)));
  404. }elseif($pk instanceof MongoDBRef){
  405. // I should probably just return it here
  406. // otherwise I will continue on
  407. return $this->_related[$name] = $this->populateReference($pk, $cname);
  408. }else{
  409. // It is just one _id
  410. $clause = array_merge($where, array($fkey => $pk));
  411. }
  412. $o = $cname::model($cname);
  413. if($relation[0] === 'one'){
  414. // Lets find it and return it
  415. return $this->_related[$name] = $o->findOne($clause);
  416. }elseif($relation[0] === 'many'){
  417. // Lets find them and return them
  418. $cursor = $o->find($clause)
  419. ->sort(isset($relation['sort']) ? $relation['sort'] : array())
  420. ->skip(isset($relation['skip']) ? $relation['skip'] : null)
  421. ->limit(isset($relation['limit']) ? $relation['limit'] : null);
  422. if(!isset($relation['cache']) || $relation['cache'] === true){
  423. return $this->_related[$name] = iterator_to_array($cursor);
  424. }
  425. }
  426. return $cursor; // FAIL SAFE
  427. }
  428. /**
  429. * @param mixed $reference Reference to populate
  430. * @param null|string $cname Class of model to populate. If not specified, populates data on current model
  431. * @return EMongoModel
  432. */
  433. public function populateReference($reference, $cname = null)
  434. {
  435. $row = MongoDBRef::get(self::$db->getDB(), $reference);
  436. $o = (is_null($cname)) ? $this : $cname::model();
  437. return $o->populateRecord($row);
  438. }
  439. /**
  440. * Returns a value indicating whether the named related object(s) has been loaded.
  441. * @param string $name the relation name
  442. * @return boolean a value indicating whether the named related object(s) has been loaded.
  443. */
  444. public function hasRelated($name)
  445. {
  446. return isset($this->_related[$name]) || array_key_exists($name, $this->_related);
  447. }
  448. /**
  449. * Sets the errors for that particular attribute
  450. * @param string $attribute
  451. * @param array $errors
  452. */
  453. public function setAttributeErrors($attribute, $errors)
  454. {
  455. $this->_errors[$attribute] = $errors;
  456. }
  457. /* THESE ERROR FUNCTIONS ARE ONLY HERE BECAUSE OF THE WAY IN WHICH PHP RESOLVES THE THE SCOPES OF VARS */
  458. // I needed to add the error handling function above but I had to include these as well
  459. /**
  460. * Returns a value indicating whether there is any validation error.
  461. * @param string $attribute attribute name. Use null to check all attributes.
  462. * @return boolean whether there is any error.
  463. */
  464. public function hasErrors($attribute = null)
  465. {
  466. if($attribute === null){
  467. return $this->_errors !== array();
  468. }
  469. return isset($this->_errors[$attribute]);
  470. }
  471. /**
  472. * Returns the errors for all attribute or a single attribute.
  473. * @param string $attribute attribute name. Use null to retrieve errors for all attributes.
  474. * @return array errors for all attributes or the specified attribute. Empty array is returned if no error.
  475. */
  476. public function getErrors($attribute = null)
  477. {
  478. if($attribute === null){
  479. return $this->_errors;
  480. }
  481. $attribute = trim(strtr($attribute, '][', '['), ']');
  482. if(strpos($attribute, '[') !== false){
  483. $prev = null;
  484. foreach(explode('[',$attribute) as $piece){
  485. if($prev === null && isset($this->errors[$piece])){
  486. $prev = $this->_errors[$piece];
  487. }elseif(isset($prev[$piece])){
  488. $prev = is_array($prev) ? $prev[$piece] : $prev->$piece;
  489. }
  490. }
  491. return $prev === null ? array() : $prev;
  492. }
  493. return isset($this->_errors[$attribute]) ? $this->_errors[$attribute] : array();
  494. }
  495. /**
  496. * Returns the first error of the specified attribute.
  497. * @param string $attribute attribute name.
  498. * @return string the error message. Null is returned if no error.
  499. */
  500. public function getError($attribute)
  501. {
  502. $attribute = trim(strtr($attribute, '][', '['), ']');
  503. if(strpos($attribute, '[') === false){
  504. return isset($this->_errors[$attribute]) ? reset($this->_errors[$attribute]) : null;
  505. }
  506. $prev = null;
  507. foreach(explode('[', $attribute) as $piece){
  508. if($prev === null && isset($this->_errors[$piece])){
  509. $prev = $this->_errors[$piece];
  510. }elseif(isset($prev[$piece])){
  511. $prev = is_array($prev) ? $prev[$piece] : $prev->$piece;
  512. }
  513. }
  514. return $prev === null ? null : reset($prev);
  515. }
  516. /**
  517. * Adds a new error to the specified attribute.
  518. * @param string $attribute attribute name
  519. * @param string $error new error message
  520. */
  521. public function addError($attribute,$error)
  522. {
  523. $this->_errors[$attribute][] = $error;
  524. }
  525. /**
  526. * Adds a list of errors.
  527. * @param array $errors a list of errors. The array keys must be attribute names.
  528. * The array values should be error messages. If an attribute has multiple errors,
  529. * these errors must be given in terms of an array.
  530. * You may use the result of {@link getErrors} as the value for this parameter.
  531. */
  532. public function addErrors($errors)
  533. {
  534. foreach($errors as $attribute => $error){
  535. if(is_array($error)){
  536. foreach($error as $e){
  537. $this->addError($attribute, $e);
  538. }
  539. }else{
  540. $this->addError($attribute, $error);
  541. }
  542. }
  543. }
  544. /**
  545. * Removes errors for all attributes or a single attribute.
  546. * @param string $attribute attribute name. Use null to remove errors for all attribute.
  547. */
  548. public function clearErrors($attribute = null)
  549. {
  550. if($attribute === null){
  551. $this->_errors = array();
  552. }else{
  553. unset($this->_errors[$attribute]);
  554. }
  555. }
  556. /**
  557. * Returns the database connection used by active record.
  558. * By default, the "mongodb" application component is used as the database connection.
  559. * You may override this method if you want to use a different database connection.
  560. * @return EMongoClient - the database connection used by active record.
  561. * @throws EMongoException
  562. */
  563. public function getDbConnection()
  564. {
  565. if(self::$db !== null){
  566. return self::$db;
  567. }
  568. self::$db = $this->getMongoComponent();
  569. if(self::$db instanceof EMongoClient){
  570. return self::$db;
  571. }
  572. throw new EMongoException(Yii::t('yii', 'MongoDB Active Record requires a "mongodb" EMongoClient application component.'));
  573. }
  574. /**
  575. * This allows you to define a custom override by default for models and the such
  576. */
  577. public function getMongoComponent()
  578. {
  579. return Yii::app()->mongodb;
  580. }
  581. /**
  582. * Cleans or rather resets the document
  583. * @return bool
  584. */
  585. public function clean()
  586. {
  587. $this->_attributes = array();
  588. $this->_related = array();
  589. // blank class properties
  590. $cache = $this->getDbConnection()->getDocumentCache(get_class($this));
  591. foreach($cache as $k => $v){
  592. $this->$k = null;
  593. }
  594. return true;
  595. }
  596. /**
  597. * Gets the formed document with MongoYii objects included
  598. * @return array
  599. */
  600. public function getDocument()
  601. {
  602. $attributes = $this->getDbConnection()->getFieldCache(get_class($this));
  603. $doc = array();
  604. if(is_array($attributes)){
  605. foreach($attributes as $field){
  606. $doc[$field] = $this->$field;
  607. }
  608. }
  609. return array_merge($doc, $this->_attributes);
  610. }
  611. /**
  612. * Gets the raw document with MongoYii objects taken out
  613. * @return array
  614. */
  615. public function getRawDocument()
  616. {
  617. return $this->filterRawDocument($this->getDocument());
  618. }
  619. /**
  620. * Filters a provided document to take out MongoYii objects.
  621. * @param array $doc
  622. * @return array
  623. */
  624. public function filterRawDocument($doc)
  625. {
  626. if(is_array($doc)){
  627. foreach($doc as $k => $v){
  628. if(is_array($v)){
  629. $doc[$k] = $this->{__FUNCTION__}($doc[$k]);
  630. }elseif($v instanceof EMongoModel || $v instanceof EMongoDocument){
  631. $doc[$k] = $doc[$k]->getRawDocument();
  632. }
  633. }
  634. }
  635. return $doc;
  636. }
  637. /**
  638. * Gets the JSON encoded document
  639. */
  640. public function getJSONDocument()
  641. {
  642. return json_encode($this->getRawDocument());
  643. }
  644. /**
  645. * Gets the BSON encoded document (never normally needed)
  646. */
  647. public function getBSONDocument()
  648. {
  649. return bson_encode($this->getRawDocument());
  650. }
  651. }