PageRenderTime 44ms CodeModel.GetById 12ms RepoModel.GetById 0ms app.codeStats 0ms

/classes/mongo/document.php

https://github.com/fjordan/mongodb-php-odm
PHP | 1241 lines | 622 code | 115 blank | 504 comment | 108 complexity | 2745e72066569b5456dadc28f62a14b2 MD5 | raw file
  1. <?php
  2. /**
  3. * This class objectifies a Mongo document and can be used with one of the following design patterns:
  4. *
  5. * 1. Table Data Gateway pattern
  6. * <code>
  7. * class Model_Post extends Mongo_Document {
  8. * protected $name = 'posts';
  9. * // All model-related code here
  10. * }
  11. * $post = Mongo_Document::factory('post', $post_id);
  12. * </code>
  13. *
  14. * 2. Row Data Gateway pattern:
  15. * <code>
  16. * class Model_Post_Collection extends Mongo_Collection {
  17. * protected $name = 'posts';
  18. * // Collection-related code here
  19. * }
  20. * class Model_Post extends Mongo_Document {
  21. * // Document-related code here
  22. * }
  23. * $post = Mongo_Document::factory('post', $post_id);
  24. * </code>
  25. *
  26. * The following examples could be used with either pattern with no differences in usage. The Row Data Gateway pattern is recommended
  27. * for more complex models to improve code organization while the Table Data Gateway pattern is recommended for simpler models.
  28. *
  29. * <code>
  30. * class Model_Document extends Mongo_Document {
  31. * public $name = 'test';
  32. * }
  33. * $document = new Model_Document(); // or Mongo_Document::factory('document');
  34. * $document->name = 'Mongo';
  35. * $document->type = 'db';
  36. * $document->save();
  37. * // db.test.save({"name":"Mongo","type":"db"});
  38. * </code>
  39. *
  40. * The _id is aliased to id by default. Other aliases can also be defined using the _aliases protected property. Aliases can be used
  41. * anywhere that a field name can be used including dot-notation for nesting.
  42. *
  43. * <code>
  44. * $id = $document->id; // MongoId
  45. * </code>
  46. *
  47. * All methods that take query parameters support JSON strings as input in addition to PHP arrays. The JSON parser is more lenient
  48. * than usual.
  49. *
  50. * <code>
  51. * $document->load('{name:"Mongo"}');
  52. * // db.test.findOne({"name":"Mongo"});
  53. * </code>
  54. *
  55. * Methods which are intended to be overridden are {before,after}_{save,load,delete} so that special actions may be
  56. * taken when these events occur:
  57. *
  58. * <code>
  59. * public function before_save()
  60. * {
  61. * $this->inc('visits');
  62. * $this->last_visit = time();
  63. * }
  64. * </code>
  65. *
  66. * When a document is saved, update will be used if the document already exists, otherwise insert will be used, determined
  67. * by the presence of an _id. A document can be modified without being loaded from the database if an _id is passed to the constructor:
  68. *
  69. * <code>
  70. * $doc = new Model_Document($id);
  71. * </code>
  72. *
  73. * Atomic operations and updates are not executed until save() is called and operations are chainable. Example:
  74. *
  75. * <code>
  76. * $doc->inc('uses.boing');
  77. * ->push('used',array('type' => 'sound', 'desc' => 'boing'));
  78. * $doc->inc('uses.bonk');
  79. * ->push('used',array('type' => 'sound', 'desc' => 'bonk'));
  80. * ->save();
  81. * // db.test.update(
  82. * // {"_id":"one"},
  83. * // {"$inc":
  84. * // {"uses.boing":1,"uses.bonk":1},
  85. * // "$pushAll":
  86. * // {"used":[{"type":"sound","desc":"boing"},{"type":"sound","desc":"bonk"}]}
  87. * // }
  88. * // );
  89. * </code>
  90. *
  91. * Documents are loaded lazily so if a property is accessed and the document is not yet loaded, it will be loaded on the first property access:
  92. *
  93. * <code>
  94. * echo "$doc->name rocks!";
  95. * // Mongo rocks!
  96. * </code>
  97. *
  98. * Documents are reloaded when accessing a property that was modified with an operator and then saved:
  99. *
  100. * <code>
  101. * in_array($doc->roles,'admin');
  102. * // TRUE
  103. * $doc->pull('roles','admin');
  104. * in_array($doc->roles,'admin');
  105. * // TRUE
  106. * $doc->save();
  107. * in_array($doc->roles,'admin');
  108. * // FALSE
  109. * </code>
  110. *
  111. * Documents can have references to other documents which will be loaded lazily and saved automatically.
  112. *
  113. * <code>
  114. * class Model_Post extends Mongo_Document {
  115. * protected $name = 'posts';
  116. * protected $_references = array('user' => array('model' => 'user'));
  117. * }
  118. *
  119. * class Model_User extends Mongo_Document {
  120. * protected $name = 'users';
  121. * }
  122. *
  123. * $user = Mongo_Document::factory('user')->set('id','colin')->set('email','colin@mollenhour.com');
  124. * $post = Mongo_Document::factory('post');
  125. * $post->user = $user;
  126. * $post->title = 'MongoDb';
  127. * $post->save()
  128. * // db.users.save({"_id":"colin","email":"colin@mollenhour.com"})
  129. * // db.posts.save({"_id":Object,"_user":"colin","title":"MongoDb"})
  130. *
  131. * $post = new Model_Post($id);
  132. * $post->_user;
  133. * // "colin" - the post was loaded lazily.
  134. * $post->user->id;
  135. * // "colin" - the user object was created lazily but not loaded.
  136. * $post->user->email;
  137. * // "colin@mollenhour.com" - now the user document was loaded as well.
  138. * </code>
  139. *
  140. * @author Colin Mollenhour
  141. * @package Mongo_Database
  142. */
  143. abstract class Mongo_Document {
  144. const SAVE_INSERT = 'insert';
  145. const SAVE_UPDATE = 'update';
  146. const SAVE_UPSERT = 'upsert';
  147. /**
  148. * Instantiate an object conforming to Mongo_Document conventions.
  149. * The document is not loaded until load() is called.
  150. *
  151. * @param string $name
  152. * @param mixed $load
  153. * @return Mongo_Document
  154. */
  155. public static function factory($name, $load = NULL)
  156. {
  157. $class = 'Model_'.implode('_',array_map('ucfirst', explode('_',$name)));
  158. return new $class($load);
  159. }
  160. /** The name of the collection within the database or the gridFS prefix if gridFS is TRUE
  161. *
  162. * If using a corresponding Mongo_Collection subclass, set this only in the Mongo_Collection subclass.
  163. *
  164. * @var string */
  165. protected $name;
  166. /** The database configuration name (passed to Mongo_Database::instance() )
  167. *
  168. * If using a corresponding Mongo_Collection subclass, set this only in the Mongo_Collection subclass.
  169. *
  170. * @var string */
  171. protected $db;
  172. /** Whether or not this collection is a gridFS collection
  173. *
  174. * If using a corresponding Mongo_Collection subclass, set this only in the Mongo_Collection subclass.
  175. *
  176. * @var boolean */
  177. protected $gridFS = FALSE;
  178. /** Definition of references existing in this document.
  179. * If 'model' is not specified it defaults to the reference name.
  180. * If 'field' is not specified it defaults to the reference name prefixed with an '_'.
  181. *
  182. * <pre>
  183. * Example Document:
  184. * {_id:1,user_id:2,_token:3}
  185. *
  186. * protected $_references = array(
  187. * 'user' => array('model' => 'user', 'field' => 'user_id'),
  188. * 'token' => NULL,
  189. * );
  190. * </pre>
  191. *
  192. * @var array */
  193. protected $_references = array();
  194. /** Definition of predefined searches for use with __call. This instantiates a collection for the target model
  195. * and initializes the search with the specified field being equal to the _id of the current object.
  196. *
  197. * <pre>
  198. * $_searches
  199. * {events: {model: 'event', field: '_user'}}
  200. * // db.event.find({_user: <_id>})
  201. * </pre>
  202. *
  203. * @var array */
  204. protected $_searches = array();
  205. /** Field name aliases. '_id' is automatically aliased to 'id'.
  206. * E.g.: {created_at: ca}
  207. * @var array */
  208. protected $_aliases = array();
  209. /** Designated place for non-persistent data storage (will not be saved to the database or after sleep)
  210. * @var array */
  211. public $__data = array();
  212. /** Internal storage of object data
  213. * @var array */
  214. protected $_object = array();
  215. /** Keep track of fields changed using __set or load_values
  216. * @var array */
  217. protected $_changed = array();
  218. /** Set of operations to perform on update/insert
  219. * @var array */
  220. protected $_operations = array();
  221. /** Keep track of data that is dirty (changed by an operation but not yet updated from database)
  222. * @var array */
  223. protected $_dirty = array();
  224. /** Storage for referenced objects
  225. * @var array */
  226. protected $_related_objects = array();
  227. /** Document loaded status:
  228. * <pre>
  229. * NULL not attempted
  230. * FALSE failed
  231. * TRUE succeeded
  232. * </pre>
  233. *
  234. * @var boolean */
  235. protected $_loaded = NULL;
  236. /** A cache of Mongo_Collection instances for performance
  237. * @static array */
  238. protected static $collections = array();
  239. /**
  240. * Instantiate a new Document object. If an id or other data is passed then it will be assumed that the
  241. * document exists in the database and updates will be performaed without loading the document first.
  242. *
  243. * @param string $id _id of the document to operate on or criteria used to load
  244. * @return Mongo_Document
  245. */
  246. public function __construct($id = NULL)
  247. {
  248. if($id !== NULL)
  249. {
  250. if(is_array($id))
  251. {
  252. foreach($id as $key => $value)
  253. {
  254. $this->_object[$this->get_field_name($key)] = $value;
  255. }
  256. }
  257. else
  258. {
  259. $this->_object['_id'] = $this->_cast('_id', $id);
  260. }
  261. }
  262. }
  263. /**
  264. * Override to cast values when they are set with untrusted data
  265. *
  266. * @param string $field The field name being set
  267. * @param mixed $value The value being set
  268. * @return mixed|\MongoId|string
  269. */
  270. protected function _cast($field, $value)
  271. {
  272. switch($field)
  273. {
  274. case '_id':
  275. // Cast _id strings to MongoIds if they convert back and forth without changing
  276. if( is_string($value) && strlen($value) == 24)
  277. {
  278. $id = new MongoId($value);
  279. if( (string) $id == $value)
  280. return $id;
  281. }
  282. }
  283. return $value;
  284. }
  285. /**
  286. * This function translates an alias to a database field name.
  287. * Aliases are defined in $this->_aliases, and id is always aliased to _id.
  288. * You can override this to disable aliases or define your own aliasing technique.
  289. *
  290. * @param string $name The aliased field name
  291. * @param boolean $dot_allowed Use FALSE if a dot is not allowed in the field name for better performance
  292. * @return string The field name used within the database
  293. */
  294. public function get_field_name($name, $dot_allowed = TRUE)
  295. {
  296. if($name == 'id' || $name == '_id') return '_id';
  297. if( ! $dot_allowed || ! strpos($name,'.'))
  298. {
  299. return (isset($this->_aliases[$name])
  300. ? $this->_aliases[$name]
  301. : $name
  302. );
  303. }
  304. $parts = explode('.', $name, 2);
  305. $parts[0] = $this->get_field_name($parts[0], FALSE);
  306. return implode('.', $parts);
  307. }
  308. /**
  309. * Returns the attributes that should be serialized.
  310. *
  311. * @return array
  312. */
  313. public function __sleep()
  314. {
  315. return array('_references', '_aliases', '_object', '_changed', '_operations', '_loaded', '_dirty');
  316. }
  317. /**
  318. * Checks if a field is set
  319. *
  320. * @param string $name
  321. * @return boolean field is set
  322. */
  323. public function __isset($name)
  324. {
  325. $name = $this->get_field_name($name, FALSE);
  326. return isset($this->_object[$name]);
  327. }
  328. /**
  329. * Unset a field
  330. *
  331. * @param string $name
  332. * @return void
  333. */
  334. public function __unset($name)
  335. {
  336. $this->_unset($name);
  337. }
  338. /**
  339. * Clear the document data
  340. *
  341. * @return Mongo_Document
  342. */
  343. public function clear()
  344. {
  345. $this->_object = $this->_changed = $this->_operations = $this->_dirty = $this->_related_objects = array();
  346. $this->_loaded = NULL;
  347. return $this;
  348. }
  349. /**
  350. * Return TRUE if field has been changed
  351. *
  352. * @param string $name field name (no parameter returns TRUE if there are *any* changes)
  353. * @return boolean field has been changed
  354. */
  355. public function is_changed($name = NULL)
  356. {
  357. if($name === NULL)
  358. {
  359. return ($this->_changed || $this->_operations);
  360. }
  361. else
  362. {
  363. $name = $this->get_field_name($name);
  364. return isset($this->_changed[$name]) || isset($this->_dirty[$name]);
  365. }
  366. }
  367. /**
  368. * Return the Mongo_Database reference (proxy to the collection's db() method)
  369. *
  370. * @return Mongo_Database
  371. */
  372. public function db()
  373. {
  374. return $this->collection()->db();
  375. }
  376. /**
  377. * Get a corresponding collection singleton
  378. *
  379. * @param boolean $fresh Pass TRUE if you don't want to get the singleton instance
  380. * @return Mongo_Collection
  381. */
  382. public function collection($fresh = FALSE)
  383. {
  384. if($fresh === TRUE)
  385. {
  386. if($this->name)
  387. {
  388. if ($this->db === NULL)
  389. {
  390. $this->db = Mongo_Database::$default;
  391. }
  392. return new Mongo_Collection($this->name, $this->db, $this->gridFS, get_class($this));
  393. }
  394. else
  395. {
  396. $class_name = $this->get_collection_class_name();
  397. return new $class_name(NULL, NULL, NULL, get_class($this));
  398. }
  399. }
  400. if($this->name)
  401. {
  402. $name = "$this->db.$this->name.$this->gridFS";
  403. if( ! isset(self::$collections[$name]))
  404. {
  405. if ($this->db === NULL)
  406. {
  407. $this->db = Mongo_Database::$default;
  408. }
  409. self::$collections[$name] = new Mongo_Collection($this->name, $this->db, $this->gridFS, get_class($this));
  410. }
  411. return self::$collections[$name];
  412. }
  413. else
  414. {
  415. $name = $this->get_collection_class_name();
  416. if( ! isset(self::$collections[$name]))
  417. {
  418. self::$collections[$name] = new $name(NULL, NULL, NULL, get_class($this));
  419. }
  420. return self::$collections[$name];
  421. }
  422. }
  423. /**
  424. * Generates the collection name
  425. * @return string
  426. */
  427. protected function get_collection_class_name()
  428. {
  429. return get_class($this).'_Collection';
  430. }
  431. /**
  432. * Current magic methods supported:
  433. *
  434. * find_<search>() - Perform predefined search (using key from $_searches)
  435. *
  436. * @param string $name
  437. * @param array $arguments
  438. * @throws Exception
  439. * @return Mongo_Collection
  440. */
  441. public function __call($name, $arguments)
  442. {
  443. // Workaround Reserved Keyword 'unset'
  444. // http://php.net/manual/en/reserved.keywords.php
  445. if($name == 'unset')
  446. {
  447. return $this->_unset($arguments[0]);
  448. }
  449. $parts = explode('_', $name, 2);
  450. if( ! isset($parts[1]))
  451. {
  452. throw new Exception('Method not found by '.get_class($this).': '.$name);
  453. }
  454. switch($parts[0])
  455. {
  456. case 'find':
  457. $search = $parts[1];
  458. if( ! isset($this->_searches[$search])){
  459. throw new Exception('Predefined search not found by '.get_class($this).': '.$search);
  460. }
  461. return Mongo_Document::factory($this->_searches[$search]['model'])
  462. ->collection(TRUE)
  463. ->find(array($this->_searches[$search]['field'] => $this->_id));
  464. break;
  465. default:
  466. throw new Exception('Method not found by '.get_class($this).': '.$name);
  467. break;
  468. }
  469. }
  470. /**
  471. * Gets one of the following:
  472. *
  473. * - A referenced object
  474. * - A search() result
  475. * - A field's value
  476. *
  477. * @param string $name field name
  478. * @return mixed
  479. */
  480. public function __get($name)
  481. {
  482. $name = $this->get_field_name($name, FALSE);
  483. // Auto-loading for special references
  484. if(array_key_exists($name, $this->_references))
  485. {
  486. if( ! isset($this->_related_objects[$name]))
  487. {
  488. $model = isset($this->_references[$name]['model']) ? $this->_references[$name]['model'] : $name;
  489. $foreign_field = isset($this->_references[$name]['foreign_field']) ? $this->_references[$name]['foreign_field'] : FALSE;
  490. if ($foreign_field) {
  491. $this->_related_objects[$name] = Mongo_Document::factory($model)
  492. ->collection(TRUE)
  493. ->find($foreign_field, $this->id);
  494. return $this->_related_objects[$name];
  495. }
  496. $id_field = isset($this->_references[$name]['field']) ? $this->_references[$name]['field'] : "_$name";
  497. $value = $this->__get($id_field);
  498. if( ! empty($this->_references[$name]['multiple']))
  499. {
  500. $this->_related_objects[$name] = Mongo_Document::factory($model)
  501. ->collection(TRUE)
  502. ->find(array('_id' => array('$in' => (array) $value)));
  503. }
  504. else
  505. {
  506. // Extract just id if value is a DBRef
  507. if(is_array($value) && isset($value['$id']))
  508. {
  509. $value = $value['$id'];
  510. }
  511. $this->_related_objects[$name] = Mongo_Document::factory($model, $value);
  512. }
  513. }
  514. return $this->_related_objects[$name];
  515. }
  516. // Reload when retrieving dirty data
  517. if($this->_loaded && empty($this->_operations) && ! empty($this->_dirty[$name]))
  518. {
  519. $this->load();
  520. }
  521. // Lazy loading!
  522. else if($this->_loaded === NULL && isset($this->_object['_id']) && ! isset($this->_changed['_id']) && $name != '_id')
  523. {
  524. $this->load();
  525. }
  526. return isset($this->_object[$name]) ? $this->_object[$name] : NULL;
  527. }
  528. /**
  529. * Magic method for setting the value of a field. In order to set the value of a nested field,
  530. * you must use the "set" method, not the magic method. Examples:
  531. *
  532. * <code>
  533. * // Works
  534. * $doc->set('address.city', 'Knoxville');
  535. *
  536. * // Does not work
  537. * $doc->address['city'] = 'Knoxville';
  538. * </code>
  539. *
  540. * @param string $name field name
  541. * @param mixed $value
  542. * @throws Exception
  543. * @return void
  544. */
  545. public function __set($name, $value)
  546. {
  547. $name = $this->get_field_name($name, FALSE);
  548. // Automatically save references to other Mongo_Document objects
  549. if(array_key_exists($name, $this->_references))
  550. {
  551. if( ! $value instanceof Mongo_Document)
  552. {
  553. throw new Exception('Cannot set reference to object that is not a Mongo_Document');
  554. }
  555. $this->_related_objects[$name] = $value;
  556. if(isset($value->_id))
  557. {
  558. $id_field = isset($this->_references[$name]['field']) ? $this->_references[$name]['field'] : "_$name";
  559. $this->__set($id_field, $value->_id);
  560. }
  561. return;
  562. }
  563. // Do not save sets that result in no change
  564. $value = $this->_cast($name, $value);
  565. if ( isset($this->_object[$name]) && $this->_object[$name] === $value)
  566. {
  567. return;
  568. }
  569. $this->_object[$name] = $value;
  570. $this->_changed[$name] = TRUE;
  571. }
  572. /**
  573. * @param $name
  574. * @return Mongo_Document
  575. */
  576. protected function _set_dirty($name)
  577. {
  578. if($pos = strpos($name,'.'))
  579. {
  580. $name = substr($name,0,$pos);
  581. }
  582. $this->_dirty[$name] = TRUE;
  583. return $this;
  584. }
  585. /**
  586. * Set the value for a key. This function must be used when updating nested documents.
  587. *
  588. * @param string $name The key of the data to update (use dot notation for embedded objects)
  589. * @param mixed $value The data to be saved
  590. * @return Mongo_Document
  591. */
  592. public function set($name, $value)
  593. {
  594. if( ! strpos($name, '.')) {
  595. $this->__set($name, $value);
  596. return $this;
  597. }
  598. $name = $this->get_field_name($name);
  599. $this->_operations['$set'][$name] = $value;
  600. return $this->_set_dirty($name);
  601. }
  602. /**
  603. * Unset a key
  604. *
  605. * Note: unset() method call for _unset() is defined in __call() method since 'unset' method name
  606. * is reserved in PHP. ( Requires PHP > 5.2.3. - http://php.net/manual/en/reserved.keywords.php )
  607. *
  608. * @param string $name The key of the data to update (use dot notation for embedded objects)
  609. * @return Mongo_Document
  610. */
  611. public function _unset($name)
  612. {
  613. $name = $this->get_field_name($name);
  614. $this->_operations['$unset'][$name] = 1;
  615. return $this->_set_dirty($name);
  616. }
  617. /**
  618. * Increment a value atomically
  619. *
  620. * @param string $name The key of the data to update (use dot notation for embedded objects)
  621. * @param mixed $value The amount to increment by (default is 1)
  622. * @return Mongo_Document
  623. */
  624. public function inc($name, $value = 1)
  625. {
  626. $name = $this->get_field_name($name);
  627. if(isset($this->_operations['$inc'][$name]))
  628. {
  629. $this->_operations['$inc'][$name] += $value;
  630. }
  631. else
  632. {
  633. $this->_operations['$inc'][$name] = $value;
  634. }
  635. return $this->_set_dirty($name);
  636. }
  637. /**
  638. * Push a vlaue to an array atomically. Can be called multiple times.
  639. *
  640. * @param string $name The key of the data to update (use dot notation for embedded objects)
  641. * @param mixed $value The value to push
  642. * @return Mongo_Document
  643. */
  644. public function push($name, $value)
  645. {
  646. $name = $this->get_field_name($name);
  647. if(isset($this->_operations['$pushAll'][$name]))
  648. {
  649. $this->_operations['$pushAll'][$name][] = $value;
  650. }
  651. else if(isset($this->_operations['$push'][$name]))
  652. {
  653. $this->_operations['$pushAll'][$name] = array($this->_operations['$push'][$name],$value);
  654. unset($this->_operations['$push'][$name]);
  655. if( ! count($this->_operations['$push']))
  656. unset($this->_operations['$push']);
  657. }
  658. else
  659. {
  660. $this->_operations['$push'][$name] = $value;
  661. }
  662. return $this->_set_dirty($name);
  663. }
  664. /**
  665. * Push an array of values to an array in the document
  666. *
  667. * @param string $name The key of the data to update (use dot notation for embedded objects)
  668. * @param array $value An array of values to push
  669. * @return Mongo_Document
  670. */
  671. public function pushAll($name, $value)
  672. {
  673. $name = $this->get_field_name($name);
  674. if(isset($this->_operations['$pushAll'][$name]))
  675. {
  676. $this->_operations['$pushAll'][$name] += $value;
  677. }
  678. else
  679. {
  680. $this->_operations['$pushAll'][$name] = $value;
  681. }
  682. return $this->_set_dirty($name);
  683. }
  684. /**
  685. * Pop a value from the end of an array
  686. *
  687. * @param string $name The key of the data to update (use dot notation for embedded objects)
  688. * @return Mongo_Document
  689. */
  690. public function pop($name)
  691. {
  692. $name = $this->get_field_name($name);
  693. $this->_operations['$pop'][$name] = 1;
  694. return $this->_set_dirty($name);
  695. }
  696. /**
  697. * Pop a value from the beginning of an array
  698. *
  699. * @param string $name The key of the data to update (use dot notation for embedded objects)
  700. * @return Mongo_Document
  701. */
  702. public function shift($name)
  703. {
  704. $name = $this->get_field_name($name);
  705. $this->_operations['$pop'][$name] = -1;
  706. return $this->_set_dirty($name);
  707. }
  708. /**
  709. * Pull (delete) a value from an array
  710. *
  711. * @param string $name The key of the data to update (use dot notation for embedded objects)
  712. * @param mixed $value
  713. * @return Mongo_Document
  714. */
  715. public function pull($name, $value)
  716. {
  717. $name = $this->get_field_name($name);
  718. if(isset($this->_operations['$pullAll'][$name]))
  719. {
  720. $this->_operations['$pullAll'][$name][] = $value;
  721. }
  722. else if(isset($this->_operations['$pull'][$name]))
  723. {
  724. $this->_operations['$pullAll'][$name] = array($this->_operations['$pull'][$name],$value);
  725. unset($this->_operations['$pull'][$name]);
  726. if( ! count($this->_operations['$pull']))
  727. unset($this->_operations['$pull']);
  728. }
  729. else
  730. {
  731. $this->_operations['$pull'][$name] = $value;
  732. }
  733. return $this->_set_dirty($name);
  734. }
  735. /**
  736. * Pull (delete) all of the given values from an array
  737. *
  738. * @param string $name The key of the data to update (use dot notation for embedded objects)
  739. * @param array $value An array of value to pull from the array
  740. * @return Mongo_Document
  741. */
  742. public function pullAll($name, $value)
  743. {
  744. $name = $this->get_field_name($name);
  745. if(isset($this->_operations['$pullAll'][$name]))
  746. {
  747. $this->_operations['$pullAll'][$name] += $value;
  748. }
  749. else
  750. {
  751. $this->_operations['$pullAll'][$name] = $value;
  752. }
  753. return $this->_set_dirty($name);
  754. }
  755. /**
  756. * Bit operators
  757. *
  758. * @param string $name The key of the data to update (use dot notation for embedded objects)
  759. * @param array $value
  760. * @return Mongo_Document
  761. */
  762. public function bit($name,$value)
  763. {
  764. $name = $this->get_field_name($name);
  765. $this->_operations['$bit'][$name] = $value;
  766. return $this->_set_dirty($name);
  767. }
  768. /**
  769. * Adds value to the array only if its not in the array already.
  770. *
  771. * @param string $name The key of the data to update (use dot notation for embedded objects)
  772. * @param mixed $value The value to add to the set
  773. * @return Mongo_Document
  774. */
  775. public function addToSet($name, $value)
  776. {
  777. $name = $this->get_field_name($name);
  778. if(isset($this->_operations['$addToSet'][$name]))
  779. {
  780. if( ! isset($this->_operations['$addToSet'][$name]['$each']))
  781. {
  782. $this->_operations['$addToSet'][$name] = array('$each' => array($this->_operations['$addToSet'][$name]));
  783. }
  784. if(isset($value['$each']))
  785. {
  786. foreach($value['$each'] as $val)
  787. {
  788. $this->_operations['$addToSet'][$name]['$each'][] = $val;
  789. }
  790. }
  791. else
  792. {
  793. $this->_operations['$addToSet'][$name]['$each'][] = $value;
  794. }
  795. }
  796. else
  797. {
  798. $this->_operations['$addToSet'][$name] = $value;
  799. }
  800. return $this->_set_dirty($name);
  801. }
  802. /**
  803. * Load all of the values in an associative array. Ignores all fields
  804. * not in the model.
  805. *
  806. * @param array $values field => value pairs
  807. * @param boolean $clean values are clean (from database)?
  808. * @return Mongo_Document
  809. */
  810. public function load_values($values, $clean = FALSE)
  811. {
  812. if($clean === TRUE)
  813. {
  814. $this->before_load();
  815. $this->_object = (array) $values;
  816. $this->_loaded = ! empty($this->_object);
  817. $this->after_load();
  818. }
  819. else
  820. {
  821. foreach ($values as $field => $value)
  822. {
  823. $this->__set($field, $value);
  824. }
  825. }
  826. return $this;
  827. }
  828. /**
  829. * Get the model data as an associative array.
  830. *
  831. * @param boolean $clean retrieve values directly from _object
  832. * @return array field => value
  833. */
  834. public function as_array( $clean = FALSE )
  835. {
  836. if($clean === TRUE)
  837. {
  838. $array = $this->_object;
  839. }
  840. else
  841. {
  842. $array = array();
  843. foreach($this->_object as $name => $value)
  844. {
  845. $array[$name] = isset($this->_object[$name]) ? $this->_object[$name] : NULL;
  846. }
  847. foreach($this->_aliases as $alias => $name)
  848. {
  849. if(isset($array[$name]))
  850. {
  851. $array[$alias] = $array[$name];
  852. unset($array[$name]);
  853. }
  854. }
  855. }
  856. return $array;
  857. }
  858. /**
  859. * Return true if the document is loaded.
  860. *
  861. * @return boolean
  862. */
  863. public function loaded()
  864. {
  865. if($this->_loaded === NULL)
  866. {
  867. $this->load();
  868. }
  869. return $this->_loaded;
  870. }
  871. /**
  872. * Load the document from the database. The first parameter may be one of:
  873. *
  874. * a falsey value - the object data will be used to construct the query
  875. * a JSON string - will be parsed and used for the query
  876. * an non-array value - the query will be assumed to be for an _id of this value
  877. * an array - the array will be used for the query
  878. *
  879. * @param array $criteria specify additional criteria
  880. * @param array $fields specify the fields to return
  881. * @throws MongoException
  882. * @return boolean TRUE if the load succeeded
  883. */
  884. public function load($criteria = array(), array $fields = array())
  885. {
  886. // Use of json for querying is allowed
  887. if(is_string($criteria) && $criteria[0] == "{")
  888. {
  889. $criteria = JSON::arr($criteria);
  890. }
  891. else if($criteria && ! is_array($criteria))
  892. {
  893. $criteria = array('_id' => $criteria);
  894. }
  895. else if(isset($this->_object['_id']))
  896. {
  897. $criteria = array('_id' => $this->_object['_id']);
  898. }
  899. else if(isset($criteria['id']))
  900. {
  901. $criteria = array('_id' => $criteria['id']);
  902. }
  903. else if( ! $criteria)
  904. {
  905. $criteria = $this->_object;
  906. }
  907. if( ! $criteria)
  908. {
  909. throw new MongoException('Cannot find '.get_class($this).' without _id or other search criteria.');
  910. }
  911. // Cast query values to the appropriate types and translate aliases
  912. $new = array();
  913. foreach($criteria as $key => $value)
  914. {
  915. $key = $this->get_field_name($key);
  916. $new[$key] = $this->_cast($key, $value);
  917. }
  918. $criteria = $new;
  919. // Translate field aliases
  920. $fields = array_map(array($this,'get_field_name'), $fields);
  921. $values = $this->collection()->__call('findOne', array($criteria, $fields));
  922. // Only clear the object if necessary
  923. if($this->_loaded !== NULL || $this->_changed || $this->_operations)
  924. {
  925. $this->clear();
  926. }
  927. $this->load_values($values, TRUE);
  928. return $this->_loaded;
  929. }
  930. /**
  931. * Save the document to the database. For newly created documents the _id will be retrieved.
  932. *
  933. * @param array|bool $options Insert options ('safe' defaults to true)
  934. * @throws MongoException
  935. * @return Mongo_Document
  936. */
  937. public function save($options = TRUE)
  938. {
  939. // Update references to referenced models
  940. $this->_update_references();
  941. // Convert old bool argument to options array
  942. if (is_bool($options)) {
  943. $options = array('safe' => $options);
  944. }
  945. if ( ! isset($options['safe'])) {
  946. $options['safe'] = TRUE;
  947. }
  948. // Insert new record if no _id or _id was set by user
  949. if( ! isset($this->_object['_id']) || isset($this->_changed['_id']))
  950. {
  951. $action = self::SAVE_INSERT;
  952. $this->before_save($action);
  953. $values = array();
  954. foreach($this->_changed as $name => $_true)
  955. {
  956. $values[$name] = $this->_object[$name];
  957. }
  958. if(empty($values))
  959. {
  960. throw new MongoException('Cannot insert empty array.');
  961. }
  962. $err = $this->collection()->insert($values, $options);
  963. if( $options['safe'] && $err['err'] )
  964. {
  965. throw new MongoException('Unable to insert '.get_class($this).': '.$err['err']);
  966. }
  967. if ( ! isset($this->_object['_id']))
  968. {
  969. // Store (assigned) MongoID in object
  970. $this->_object['_id'] = $values['_id'];
  971. $this->_loaded = TRUE;
  972. }
  973. // Save any additional operations
  974. /** @todo Combine operations into the insert when possible to avoid this update */
  975. if($this->_operations)
  976. {
  977. if( ! $this->collection()->update(array('_id' => $this->_object['_id']), $this->_operations))
  978. {
  979. $err = $this->db()->last_error();
  980. throw new MongoException('Update of '.get_class($this).' failed: '.$err['err']);
  981. }
  982. }
  983. }
  984. // Update assumed existing document
  985. else
  986. {
  987. $action = self::SAVE_UPDATE;
  988. $this->before_save($action);
  989. if($this->_changed)
  990. {
  991. foreach($this->_changed as $name => $_true)
  992. {
  993. $this->_operations['$set'][$name] = $this->_object[$name];
  994. }
  995. }
  996. if($this->_operations)
  997. {
  998. if( ! $this->collection()->update(array('_id' => $this->_object['_id']), $this->_operations))
  999. {
  1000. $err = $this->db()->last_error();
  1001. throw new MongoException('Update of '.get_class($this).' failed: '.$err['err']);
  1002. }
  1003. }
  1004. }
  1005. $this->_changed = $this->_operations = array();
  1006. $this->after_save($action);
  1007. return $this;
  1008. }
  1009. /**
  1010. * Updates references but does not save models to avoid infinite loops
  1011. */
  1012. protected function _update_references()
  1013. {
  1014. foreach($this->_references as $name => $ref)
  1015. {
  1016. if(isset($this->_related_objects[$name]) && $this->_related_objects[$name] instanceof Mongo_Document)
  1017. {
  1018. $model = $this->_related_objects[$name];
  1019. $id_field = isset($ref['field']) ? $ref['field'] : "_$name";
  1020. if( ! $this->__isset($id_field) || $this->__get($id_field) != $model->_id)
  1021. {
  1022. $this->__set($id_field, $model->_id);
  1023. }
  1024. }
  1025. }
  1026. }
  1027. /**
  1028. * Override this method to take certain actions before the data is saved
  1029. *
  1030. * @param string $action The type of save action, one of Mongo_Document::SAVE_*
  1031. */
  1032. protected function before_save($action){}
  1033. /**
  1034. * Override this method to take actions after data is saved
  1035. *
  1036. * @param string $action The type of save action, one of Mongo_Document::SAVE_*
  1037. */
  1038. protected function after_save($action){}
  1039. /**
  1040. * Override this method to take actions before the values are loaded
  1041. */
  1042. protected function before_load(){}
  1043. /**
  1044. * Override this method to take actions after the values are loaded
  1045. */
  1046. protected function after_load(){}
  1047. /**
  1048. * Override this method to take actions before the document is deleted
  1049. */
  1050. protected function before_delete(){}
  1051. /**
  1052. * Override this method to take actions after the document is deleted
  1053. */
  1054. protected function after_delete(){}
  1055. /**
  1056. * Upsert the document, does not retrieve the _id of the upserted document.
  1057. *
  1058. * @param array $operations
  1059. * @throws MongoException
  1060. * @return Mongo_Document
  1061. */
  1062. public function upsert($operations = array())
  1063. {
  1064. if( ! $this->_object)
  1065. {
  1066. throw new MongoException('Cannot upsert '.get_class($this).': no criteria');
  1067. }
  1068. $this->before_save(self::SAVE_UPSERT);
  1069. $operations = self::array_merge_recursive_distinct($this->_operations, $operations);
  1070. if( ! $this->collection()->update($this->_object, $operations, array('upsert' => TRUE)))
  1071. {
  1072. $err = $this->db()->last_error();
  1073. throw new MongoException('Upsert of '.get_class($this).' failed: '.$err['err']);
  1074. }
  1075. $this->_changed = $this->_operations = array();
  1076. $this->after_save(self::SAVE_UPSERT);
  1077. return $this;
  1078. }
  1079. /**
  1080. * Delete the current document using the current data. The document does not have to be loaded.
  1081. * Use $doc->collection()->remove($criteria) to delete multiple documents.
  1082. *
  1083. * @throws MongoException
  1084. * @return Mongo_Document
  1085. */
  1086. public function delete()
  1087. {
  1088. if( ! isset($this->_object['_id']))
  1089. {
  1090. throw new MongoException('Cannot delete '.get_class($this).' without the _id.');
  1091. }
  1092. $this->before_delete();
  1093. $criteria = array('_id' => $this->_object['_id']);
  1094. if( ! $this->collection()->remove($criteria, array('justOne' => true)))
  1095. {
  1096. throw new MongoException('Failed to delete '.get_class($this));
  1097. }
  1098. $this->clear();
  1099. $this->after_delete();
  1100. return $this;
  1101. }
  1102. /**
  1103. * array_merge_recursive_distinct does not change the datatypes of the values in the arrays.
  1104. * @param array $array1
  1105. * @param array $array2
  1106. * @return array
  1107. * @author Daniel <daniel (at) danielsmedegaardbuus (dot) dk>
  1108. * @author Gabriel Sobrinho <gabriel (dot) sobrinho (at) gmail (dot) com>
  1109. */
  1110. protected static function array_merge_recursive_distinct ( array &$array1, array &$array2 )
  1111. {
  1112. $merged = $array1;
  1113. foreach ( $array2 as $key => &$value )
  1114. {
  1115. if ( is_array ( $value ) && isset ( $merged [$key] ) && is_array ( $merged [$key] ) )
  1116. {
  1117. $merged [$key] = self::array_merge_recursive_distinct ( $merged [$key], $value );
  1118. }
  1119. else
  1120. {
  1121. $merged [$key] = $value;
  1122. }
  1123. }
  1124. return $merged;
  1125. }
  1126. }