PageRenderTime 65ms CodeModel.GetById 26ms RepoModel.GetById 0ms app.codeStats 1ms

/library/Shanty/Mongo/Document.php

https://bitbucket.org/ilyabazhenov/speakplace
PHP | 1263 lines | 677 code | 202 blank | 384 comment | 132 complexity | 7a8d6557bad537fc1fffac6c22d15891 MD5 | raw file
  1. <?php
  2. require_once 'Shanty/Mongo/Exception.php';
  3. require_once 'Shanty/Mongo/Collection.php';
  4. require_once 'Shanty/Mongo/Iterator/Default.php';
  5. /**
  6. * @category Shanty
  7. * @package Shanty_Mongo
  8. * @copyright Shanty Tech Pty Ltd
  9. * @license New BSD License
  10. * @author Coen Hyde
  11. */
  12. class Shanty_Mongo_Document extends Shanty_Mongo_Collection implements ArrayAccess, Countable, IteratorAggregate
  13. {
  14. protected static $_requirements = array(
  15. '_id' => 'Validator:MongoId',
  16. '_type' => 'Array'
  17. );
  18. protected $_docRequirements = array();
  19. protected $_data = array();
  20. protected $_cleanData = array();
  21. protected $_config = array(
  22. 'new' => true,
  23. 'connectionGroup' => null,
  24. 'db' => null,
  25. 'collection' => null,
  26. 'pathToDocument' => null,
  27. 'criteria' => array(),
  28. 'parentIsDocumentSet' => false,
  29. 'requirementModifiers' => array(),
  30. 'locked' => false
  31. );
  32. protected $_operations = array();
  33. protected $_references = null;
  34. public function __construct($data = array(), $config = array())
  35. {
  36. // Make sure mongo is initialised
  37. Shanty_Mongo::init();
  38. $this->_config = array_merge($this->_config, $config);
  39. $this->_references = new SplObjectStorage();
  40. // If not connected and this is a new root document, figure out the db and collection
  41. if ($this->isNewDocument() && $this->isRootDocument() && !$this->isConnected()) {
  42. $this->setConfigAttribute('connectionGroup', static::getConnectionGroupName());
  43. $this->setConfigAttribute('db', static::getDbName());
  44. $this->setConfigAttribute('collection', static::getCollectionName());
  45. }
  46. // Get collection requirements
  47. $this->_docRequirements = static::getCollectionRequirements();
  48. // apply requirements requirement modifiers
  49. $this->applyRequirements($this->_config['requirementModifiers'], false);
  50. // Store data
  51. $this->_cleanData = $data;
  52. // Initialize input data
  53. if ($this->isNewDocument() && is_array($data)) {
  54. foreach ($data as $key => $value) {
  55. $this->getProperty($key);
  56. }
  57. }
  58. // Create document id if one is required
  59. if ($this->isNewDocument() && ($this->hasKey() || (isset($this->_config['hasId']) && $this->_config['hasId']))) {
  60. $this->_data['_id'] = new MongoId();
  61. $this->_data['_type'] = static::getCollectionInheritance();
  62. }
  63. // If has key then add it to the update criteria
  64. if ($this->hasKey()) {
  65. $this->setCriteria($this->getPathToProperty('_id'), $this->getId());
  66. }
  67. $this->init();
  68. }
  69. protected function init()
  70. {
  71. }
  72. protected function preInsert()
  73. {
  74. }
  75. protected function postInsert()
  76. {
  77. }
  78. protected function preUpdate()
  79. {
  80. }
  81. protected function postUpdate()
  82. {
  83. }
  84. protected function preSave()
  85. {
  86. }
  87. protected function postSave()
  88. {
  89. }
  90. protected function preDelete()
  91. {
  92. }
  93. protected function postDelete()
  94. {
  95. }
  96. /**
  97. * Get this document's id
  98. *
  99. * @return MongoId
  100. */
  101. public function getId()
  102. {
  103. return $this->_id;
  104. }
  105. /**
  106. * Does this document have an id
  107. *
  108. * @return boolean
  109. */
  110. public function hasId()
  111. {
  112. return !is_null($this->getId());
  113. }
  114. /**
  115. * Get the inheritance of this document
  116. *
  117. * @return array
  118. */
  119. public function getInheritance()
  120. {
  121. return $this->_type;
  122. }
  123. /**
  124. * Get a config attribute
  125. *
  126. * @param string $attribute
  127. */
  128. public function getConfigAttribute($attribute)
  129. {
  130. if (!$this->hasConfigAttribute($attribute)) return null;
  131. return $this->_config[$attribute];
  132. }
  133. /**
  134. * Set a config attribute
  135. *
  136. * @param string $attribute
  137. * @param unknown_type $value
  138. */
  139. public function setConfigAttribute($attribute, $value)
  140. {
  141. $this->_config[$attribute] = $value;
  142. }
  143. /**
  144. * Determine if a config attribute is set
  145. *
  146. * @param string $attribute
  147. */
  148. public function hasConfigAttribute($attribute)
  149. {
  150. return array_key_exists($attribute, $this->_config);
  151. }
  152. /**
  153. * Is this document connected to a db and collection
  154. */
  155. public function isConnected()
  156. {
  157. return (!is_null($this->getConfigAttribute('connectionGroup')) && !is_null($this->getConfigAttribute('db')) && !is_null($this->getConfigAttribute('collection')));
  158. }
  159. /**
  160. * Is this document locked
  161. *
  162. * @return boolean
  163. */
  164. public function isLocked()
  165. {
  166. return $this->getConfigAttribute('locked');
  167. }
  168. /**
  169. * Get the path to this document from the root document
  170. *
  171. * @return string
  172. */
  173. public function getPathToDocument()
  174. {
  175. return $this->getConfigAttribute('pathToDocument');
  176. }
  177. /**
  178. * Set the path to this document from the root document
  179. * @param unknown_type $path
  180. */
  181. public function setPathToDocument($path)
  182. {
  183. $this->setConfigAttribute('pathToDocument', $path);
  184. }
  185. /**
  186. * Get the full path from the root document to a property
  187. *
  188. * @param $property
  189. * @return string
  190. */
  191. public function getPathToProperty($property)
  192. {
  193. if ($this->isRootDocument()) return $property;
  194. return $this->getPathToDocument().'.'.$property;
  195. }
  196. /**
  197. * Is this document a root document
  198. *
  199. * @return boolean
  200. */
  201. public function isRootDocument()
  202. {
  203. return is_null($this->getPathToDocument());
  204. }
  205. /**
  206. * Determine if this document has a key
  207. *
  208. * @return boolean
  209. */
  210. public function hasKey()
  211. {
  212. return ($this->isRootDocument() && $this->isConnected());
  213. }
  214. /**
  215. * Is this document a child element of a document set
  216. *
  217. * @return boolean
  218. */
  219. public function isParentDocumentSet()
  220. {
  221. return $this->_config['parentIsDocumentSet'];
  222. }
  223. /**
  224. * Determine if the document has certain criteria
  225. *
  226. * @return boolean
  227. */
  228. public function hasCriteria($property)
  229. {
  230. return array_key_exists($property, $this->_config['criteria']);
  231. }
  232. /**
  233. * Add criteria
  234. *
  235. * @param string $property
  236. * @param MongoId $id
  237. */
  238. public function setCriteria($property = null, $value = null)
  239. {
  240. $this->_config['criteria'][$property] = $value;
  241. }
  242. /**
  243. * Get criteria
  244. *
  245. * @param string $property
  246. * @return mixed
  247. */
  248. public function getCriteria($property = null)
  249. {
  250. if (is_null($property)) return $this->_config['criteria'];
  251. if (!array_key_exists($property, $this->_config['criteria'])) return null;
  252. return $this->_config['criteria'][$property];
  253. }
  254. /**
  255. * Fetch an instance of MongoDb
  256. *
  257. * @param boolean $writable
  258. * @return MongoDb
  259. */
  260. public function _getMongoDb($writable = true)
  261. {
  262. if (is_null($this->getConfigAttribute('db'))) {
  263. require_once 'Shanty/Mongo/Exception.php';
  264. throw new Shanty_Mongo_Exception('Can not fetch instance of MongoDb. Document is not connected to a db.');
  265. }
  266. if ($writable) $connection = Shanty_Mongo::getWriteConnection($this->getConfigAttribute('connectionGroup'));
  267. else $connection = Shanty_Mongo::getReadConnection($this->getConfigAttribute('connectionGroup'));
  268. return $connection->selectDB($this->getConfigAttribute('db'));
  269. }
  270. /**
  271. * Fetch an instance of MongoCollection
  272. *
  273. * @param boolean $writable
  274. * @return MongoCollection
  275. */
  276. public function _getMongoCollection($writable = true)
  277. {
  278. if (is_null($this->getConfigAttribute('collection'))) {
  279. require_once 'Shanty/Mongo/Exception.php';
  280. throw new Shanty_Mongo_Exception('Can not fetch instance of MongoCollection. Document is not connected to a collection.');
  281. }
  282. return $this->_getMongoDb($writable)->selectCollection($this->getConfigAttribute('collection'));
  283. }
  284. /**
  285. * Apply a set of requirements
  286. *
  287. * @param array $requirements
  288. */
  289. public function applyRequirements($requirements, $dirty = true)
  290. {
  291. if ($dirty) {
  292. $requirements = static::makeRequirementsTidy($requirements);
  293. }
  294. $this->_docRequirements = static::mergeRequirements($this->_docRequirements, $requirements);
  295. }
  296. /**
  297. * Test if this document has a particular requirement
  298. *
  299. * @param string $property
  300. * @param string $requirement
  301. */
  302. public function hasRequirement($property, $requirement)
  303. {
  304. if (!array_key_exists($property, $this->_docRequirements)) return false;
  305. switch($requirement) {
  306. case 'Document':
  307. case 'DocumentSet':
  308. foreach ($this->_docRequirements[$property] as $requirementSearch => $params) {
  309. $standardClass = 'Shanty_Mongo_'.$requirement;
  310. // Return basic document or document set class if requirement matches
  311. if ($requirementSearch == $requirement) {
  312. return $standardClass;
  313. }
  314. // Find the document class
  315. $matches = array();
  316. preg_match("/^{$requirement}:([A-Za-z][\w\-]*)$/", $requirementSearch, $matches);
  317. if (!empty($matches)) {
  318. if (!class_exists($matches[1])) {
  319. require_once 'Shanty/Mongo/Exception.php';
  320. throw new Shanty_Mongo_Exception("$requirement class of '{$matches[1]}' does not exist");
  321. }
  322. if (!is_subclass_of($matches[1], $standardClass)) {
  323. require_once 'Shanty/Mongo/Exception.php';
  324. throw new Shanty_Mongo_Exception("$requirement of '{$matches[1]}' sub is not a class of $standardClass does not exist");
  325. }
  326. return $matches[1];
  327. }
  328. }
  329. return false;
  330. }
  331. return array_key_exists($requirement, $this->_docRequirements[$property]);
  332. }
  333. /**
  334. * Get all requirements. If prefix is provided then only the requirements for
  335. * the properties that start with prefix will be returned.
  336. *
  337. * @param string $prefix
  338. */
  339. public function getRequirements($prefix = null)
  340. {
  341. // If no prefix is provided return all requirements
  342. if (is_null($prefix)) return $this->_docRequirements;
  343. // Find requirements for all properties starting with prefix
  344. $properties = array_filter(array_keys($this->_docRequirements), function($value) use ($prefix) {
  345. return (substr_compare($value, $prefix, 0, strlen($prefix)) == 0 && strlen($value) > strlen($prefix));
  346. });
  347. $requirements = array_intersect_key($this->_docRequirements, array_flip($properties));
  348. // Remove prefix from requirement key
  349. $newRequirements = array();
  350. array_walk($requirements, function($value, $key) use ($prefix, &$newRequirements) {
  351. $newRequirements[substr($key, strlen($prefix))] = $value;
  352. });
  353. return $newRequirements;
  354. }
  355. /**
  356. * Add a requirement to a property
  357. *
  358. * @param string $property
  359. * @param string $requirement
  360. */
  361. public function addRequirement($property, $requirement, $options = null)
  362. {
  363. if (!array_key_exists($property, $this->_docRequirements)) {
  364. $this->_docRequirements[$property] = array();
  365. }
  366. $this->_docRequirements[$property][$requirement] = $options;
  367. }
  368. /**
  369. * Remove a requirement from a property
  370. *
  371. * @param string $property
  372. * @param string $requirement
  373. */
  374. public function removeRequirement($property, $requirement)
  375. {
  376. if (!array_key_exists($property, $this->_docRequirements)) return;
  377. foreach ($this->_docRequirements[$property] as $requirementItem => $options) {
  378. if ($requirement === $requirementItem) {
  379. unset($this->_docRequirements[$property][$requirementItem]);
  380. }
  381. }
  382. }
  383. /**
  384. * Get all the properties with a particular requirement
  385. *
  386. * @param array $requirement
  387. */
  388. public function getPropertiesWithRequirement($requirement)
  389. {
  390. $properties = array();
  391. foreach ($this->_docRequirements as $property => $requirementList) {
  392. if (strpos($property, '.') > 0) continue;
  393. if (array_key_exists($requirement, $requirementList)) {
  394. $properties[] = $property;
  395. }
  396. }
  397. return $properties;
  398. }
  399. /**
  400. * Get all validators attached to a property
  401. *
  402. * @param String $property Name of property
  403. * @return Zend_Validate
  404. **/
  405. public function getValidators($property)
  406. {
  407. $validators = new Zend_Validate();
  408. // Return if no requirements are set for this property
  409. if (!array_key_exists($property, $this->_docRequirements)) return $validators;
  410. foreach ($this->_docRequirements[$property] as $requirement => $options) {
  411. // continue if requirement does not exist or is not a validator requirement
  412. $validator = Shanty_Mongo::retrieveRequirement($requirement, $options);
  413. if (!$validator || !($validator instanceof Zend_Validate_Interface)) continue;
  414. $validators->addValidator($validator);
  415. }
  416. return $validators;
  417. }
  418. /**
  419. * Get all filters attached to a property
  420. *
  421. * @param String $property
  422. * @return Zend_Filter
  423. */
  424. public function getFilters($property)
  425. {
  426. $filters = new Zend_Filter();
  427. // Return if no requirements are set for this property
  428. if (!array_key_exists($property, $this->_docRequirements)) return $filters;
  429. foreach ($this->_docRequirements[$property] as $requirement => $options) {
  430. // continue if requirement does not exist or is not a filter requirement
  431. $filter = Shanty_Mongo::retrieveRequirement($requirement, $options);
  432. if (!$filter || !($filter instanceof Zend_Filter_Interface)) continue;
  433. $filters->addFilter($filter);
  434. }
  435. return $filters;
  436. }
  437. /**
  438. * Test if a value is valid against a property
  439. *
  440. * @param String $property
  441. * @param Boolean $value
  442. */
  443. public function isValid($property, $value)
  444. {
  445. $validators = $this->getValidators($property);
  446. return $validators->isValid($value);
  447. }
  448. /**
  449. * Get a property
  450. *
  451. * @param mixed $property
  452. */
  453. public function getProperty($property)
  454. {
  455. // If property exists and initialised then return it
  456. if (array_key_exists($property, $this->_data)) {
  457. return $this->_data[$property];
  458. }
  459. // Fetch clean data for this property
  460. if (array_key_exists($property, $this->_cleanData)) {
  461. $data = $this->_cleanData[$property];
  462. }
  463. else {
  464. $data = array();
  465. }
  466. // If data is not an array then we can do nothing else with it
  467. if (!is_array($data)) {
  468. $this->_data[$property] = $data;
  469. return $this->_data[$property];
  470. }
  471. // If property is supposed to be an array then initialise an array
  472. if ($this->hasRequirement($property, 'Array')) {
  473. return $this->_data[$property] = $data;
  474. }
  475. // If property is a reference to another document then fetch the reference document
  476. $db = $this->getConfigAttribute('db');
  477. if (MongoDBRef::isRef($data)) {
  478. $collection = $data['$ref'];
  479. $data = MongoDBRef::get($this->_getMongoDB(false), $data);
  480. // If this is a broken reference then no point keeping it for later
  481. if (!$data) {
  482. $this->_data[$property] = null;
  483. return $this->_data[$property];
  484. }
  485. $reference = true;
  486. }
  487. else {
  488. $collection = $this->getConfigAttribute('collection');
  489. $reference = false;
  490. }
  491. // Find out the class name of the document or document set we are loaded
  492. if ($className = $this->hasRequirement($property, 'DocumentSet')) {
  493. $docType = 'Shanty_Mongo_DocumentSet';
  494. }
  495. else {
  496. $className = $this->hasRequirement($property, 'Document');
  497. // Load a document anyway so long as $data is not empty
  498. if (!$className && !empty($data)) {
  499. $className = 'Shanty_Mongo_Document';
  500. }
  501. if ($className) $docType = 'Shanty_Mongo_Document';
  502. }
  503. // Nothing else to do
  504. if (!$className) return null;
  505. // Configure property for document/documentSet usage
  506. $config = array();
  507. $config['new'] = empty($data);
  508. $config['connectionGroup'] = $this->getConfigAttribute('connectionGroup');
  509. $config['db'] = $this->getConfigAttribute('db');
  510. $config['collection'] = $collection;
  511. $config['requirementModifiers'] = $this->getRequirements($property.'.');
  512. $config['hasId'] = $this->hasRequirement($property, 'hasId');
  513. if (!$reference) {
  514. $config['pathToDocument'] = $this->getPathToProperty($property);
  515. $config['criteria'] = $this->getCriteria();
  516. }
  517. // Initialise document
  518. $document = new $className($data, $config);
  519. // if this document was a reference then remember that
  520. if ($reference) {
  521. $this->_references->attach($document);
  522. }
  523. $this->_data[$property] = $document;
  524. return $this->_data[$property];
  525. }
  526. /**
  527. * Set a property
  528. *
  529. * @param mixed $property
  530. * @param mixed $value
  531. */
  532. public function setProperty($property, $value)
  533. {
  534. if (substr($property, 0, 1) == '_') {
  535. require_once 'Shanty/Mongo/Exception.php';
  536. throw new Shanty_Mongo_Exception("Can not set private property '$property'");
  537. }
  538. $validators = $this->getValidators($property);
  539. // Throw exception if value is not valid
  540. if (!is_null($value) && !$validators->isValid($value)) {
  541. require_once 'Shanty/Mongo/Exception.php';
  542. throw new Shanty_Mongo_Exception(implode($validators->getMessages(), "\n"));
  543. }
  544. // Unset property
  545. if (is_null($value)) {
  546. $this->_data[$property] = null;
  547. return;
  548. }
  549. if ($value instanceof Shanty_Mongo_Document && !$this->hasRequirement($property, 'AsReference')) {
  550. if (!$value->isNewDocument() || !$value->isRootDocument()) {
  551. $documentClass = get_class($value);
  552. $value = new $documentClass($value->export(), array('new' => false, 'pathToDocument' => $this->getPathToProperty($property)));
  553. }
  554. else {
  555. $value->setPathToDocument($this->getPathToProperty($property));
  556. }
  557. $value->setConfigAttribute('connectionGroup', $this->getConfigAttribute('connectionGroup'));
  558. $value->setConfigAttribute('db', $this->getConfigAttribute('db'));
  559. $value->setConfigAttribute('collection', $this->getConfigAttribute('collection'));
  560. $value->setConfigAttribute('criteria', $this->getCriteria());
  561. $value->applyRequirements($this->getRequirements($property.'.'));
  562. }
  563. // Filter value
  564. $value = $this->getFilters($property)->filter($value);
  565. $this->_data[$property] = $value;
  566. }
  567. /**
  568. * Determine if this document has a property
  569. *
  570. * @param $property
  571. * @return boolean
  572. */
  573. public function hasProperty($property)
  574. {
  575. // If property has been initialised
  576. if (array_key_exists($property, $this->_data)) {
  577. return !is_null($this->_data[$property]);
  578. }
  579. // If property has not been initialised
  580. if (array_key_exists($property, $this->_cleanData)) {
  581. return !is_null($this->_cleanData[$property]);
  582. }
  583. return false;
  584. }
  585. /**
  586. * Get a list of all property keys in this document
  587. */
  588. public function getPropertyKeys()
  589. {
  590. $keyList = array();
  591. $doNoCount = array();
  592. foreach ($this->_data as $property => $value) {
  593. if (($value instanceof Shanty_Mongo_Document && !$value->isEmpty()) ||
  594. (!($value instanceof Shanty_Mongo_Document) && !is_null($value))) {
  595. $keyList[] = $property;
  596. }
  597. else {
  598. $doNoCount[] = $property;
  599. }
  600. }
  601. foreach ($this->_cleanData as $property => $value) {
  602. if (in_array($property, $keyList, true) || in_array($property, $doNoCount, true)) continue;
  603. if (!is_null($value)) $keyList[] = $property;
  604. }
  605. return $keyList;
  606. }
  607. /**
  608. * Create a reference to this document
  609. *
  610. * @return array
  611. */
  612. public function createReference()
  613. {
  614. if (!$this->isRootDocument()) {
  615. require_once 'Shanty/Mongo/Exception.php';
  616. throw new Shanty_Mongo_Exception('Can not create reference. Document is not a root document');
  617. }
  618. if (!$this->isConnected()) {
  619. require_once 'Shanty/Mongo/Exception.php';
  620. throw new Shanty_Mongo_Exception('Can not create reference. Document does not connected to a db and collection');
  621. }
  622. return MongoDBRef::create($this->getConfigAttribute('collection'), $this->getId());
  623. }
  624. /**
  625. * Test to see if a document is a reference in this document
  626. *
  627. * @param Shanty_Mongo_Document $document
  628. * @return boolean
  629. */
  630. public function isReference(Shanty_Mongo_Document $document)
  631. {
  632. return $this->_references->contains($document);
  633. }
  634. /**
  635. * Export all data
  636. *
  637. * @return array
  638. */
  639. public function export()
  640. {
  641. $exportData = $this->_cleanData;
  642. foreach ($this->_data as $property => $value) {
  643. // If property has been deleted
  644. if (is_null($value)) {
  645. unset($exportData[$property]);
  646. continue;
  647. }
  648. // If property is a document
  649. if ($value instanceof Shanty_Mongo_Document) {
  650. // Make when exporting from a documentset look up the correct requirement index
  651. if ($this instanceof Shanty_Mongo_DocumentSet) {
  652. $requirementIndex = Shanty_Mongo_DocumentSet::DYNAMIC_INDEX;
  653. }
  654. else {
  655. $requirementIndex = $property;
  656. }
  657. // If document is supposed to be a reference
  658. if ($this->hasRequirement($requirementIndex, 'AsReference') || $this->isReference($value)) {
  659. $exportData[$property] = $value->createReference();
  660. continue;
  661. }
  662. $data = $value->export();
  663. if (!empty($data)) {
  664. $exportData[$property] = $data;
  665. }
  666. continue;
  667. }
  668. $exportData[$property] = $value;
  669. }
  670. // make sure required properties are not empty
  671. $requiredProperties = $this->getPropertiesWithRequirement('Required');
  672. foreach ($requiredProperties as $property) {
  673. if (!isset($exportData[$property]) || (is_array($exportData[$property]) && empty($exportData[$property]))) {
  674. require_once 'Shanty/Mongo/Exception.php';
  675. throw new Shanty_Mongo_Exception("Property '{$property}' must not be null.");
  676. }
  677. }
  678. return $exportData;
  679. }
  680. /**
  681. * Is this document a new document
  682. *
  683. * @return boolean
  684. */
  685. public function isNewDocument()
  686. {
  687. return ($this->_config['new']);
  688. }
  689. /**
  690. * Test to see if this document is empty
  691. *
  692. * @return Boolean
  693. */
  694. public function isEmpty()
  695. {
  696. $doNoCount = array();
  697. foreach ($this->_data as $property => $value) {
  698. if ($value instanceof Shanty_Mongo_Document) {
  699. if (!$value->isEmpty()) return false;
  700. }
  701. elseif (!is_null($value)) {
  702. return false;
  703. }
  704. $doNoCount[] = $property;
  705. }
  706. foreach ($this->_cleanData as $property => $value) {
  707. if (in_array($property, $doNoCount)) {
  708. continue;
  709. }
  710. if (!is_null($value)) {
  711. return false;
  712. }
  713. }
  714. return true;
  715. }
  716. /**
  717. * Convert data changes into operations
  718. *
  719. * @param array $data
  720. */
  721. public function processChanges(array $data = array())
  722. {
  723. foreach ($data as $property => $value) {
  724. if ($property === '_id') continue;
  725. if (!array_key_exists($property, $this->_cleanData)) {
  726. $this->addOperation('$set', $property, $value);
  727. continue;
  728. }
  729. $newValue = $value;
  730. $oldValue = $this->_cleanData[$property];
  731. if (MongoDBRef::isRef($newValue) && MongoDBRef::isRef($oldValue)) {
  732. $newValue['$id'] = $newValue['$id']->__toString();
  733. $oldValue['$id'] = $oldValue['$id']->__toString();
  734. }
  735. if ($newValue !== $oldValue) {
  736. $this->addOperation('$set', $property, $value);
  737. }
  738. }
  739. foreach ($this->_cleanData as $property => $value) {
  740. if (array_key_exists($property, $data)) continue;
  741. $this->addOperation('$unset', $property, 1);
  742. }
  743. }
  744. /**
  745. * Save this document
  746. *
  747. * @param boolean $entierDocument Force the saving of the entier document, instead of just the changes
  748. * @param boolean $safe If FALSE, the program continues executing without waiting for a database response. If TRUE, the program will wait for the database response and throw a MongoCursorException if the update did not succeed
  749. * @return boolean Result of save
  750. */
  751. public function save($entierDocument = false, $safe = true)
  752. {
  753. if (!$this->isConnected()) {
  754. require_once 'Shanty/Mongo/Exception.php';
  755. throw new Shanty_Mongo_Exception('Can not save documet. Document is not connected to a db and collection');
  756. }
  757. if ($this->isLocked()) {
  758. require_once 'Shanty/Mongo/Exception.php';
  759. throw new Shanty_Mongo_Exception('Can not save documet. Document is locked.');
  760. }
  761. ## execute pre hooks
  762. if ($this->isNewDocument()) $this->preInsert();
  763. else $this->preUpdate();
  764. $this->preSave();
  765. $exportData = $this->export();
  766. if ($this->isRootDocument() && ($this->isNewDocument() || $entierDocument)) {
  767. // Save the entier document
  768. $operations = $exportData;
  769. }
  770. else {
  771. // Update an existing document and only send the changes
  772. if (!$this->isRootDocument()) {
  773. // are we updating a child of an array?
  774. if ($this->isNewDocument() && $this->isParentDocumentSet()) {
  775. $this->_operations['$push'][$this->getPathToDocument()] = $exportData;
  776. $exportData = array();
  777. /**
  778. * We need to lock this document because it has an incomplete document path and there is no way to find out it's true path.
  779. * Locking prevents overriding the parent array on another save() after this save().
  780. */
  781. $this->setConfigAttribute('locked', true);
  782. }
  783. }
  784. // Convert all data changes into sets and unsets
  785. $this->processChanges($exportData);
  786. $operations = $this->getOperations(true);
  787. // There are no changes, return so we don't blank the object
  788. if (empty($operations)) {
  789. return true;
  790. }
  791. }
  792. $result = $this->_getMongoCollection(true)->update($this->getCriteria(), $operations, array('upsert' => true, 'safe' => $safe));
  793. $this->_data = array();
  794. $this->_cleanData = $exportData;
  795. $this->purgeOperations(true);
  796. // Run post hooks
  797. if ($this->isNewDocument()) $this->postInsert();
  798. else $this->postUpdate();
  799. $this->postSave();
  800. // This is not a new document anymore
  801. $this->setConfigAttribute('new', false);
  802. return $result;
  803. }
  804. /**
  805. * Save this document without waiting for a response from the server
  806. *
  807. * @param boolean $entierDocument Force the saving of the entier document, instead of just the changes
  808. * @return boolean Result of save
  809. */
  810. public function saveUnsafe($entierDocument = false)
  811. {
  812. return $this->save($entierDocument, false);
  813. }
  814. /**
  815. * Delete this document
  816. *
  817. * $return boolean Result of delete
  818. */
  819. public function delete($safe = true)
  820. {
  821. if (!$this->isConnected()) {
  822. require_once 'Shanty/Mongo/Exception.php';
  823. throw new Shanty_Mongo_Exception('Can not delete document. Document is not connected to a db and collection');
  824. }
  825. if ($this->isLocked()) {
  826. require_once 'Shanty/Mongo/Exception.php';
  827. throw new Shanty_Mongo_Exception('Can not save documet. Document is locked.');
  828. }
  829. $mongoCollection = $this->_getMongoCollection(true);
  830. // Execute pre delete hook
  831. $this->preDelete();
  832. if (!$this->isRootDocument()) {
  833. $result = $mongoCollection->update($this->getCriteria(), array('$unset' => array($this->getPathToDocument() => 1)), array('safe' => $safe));
  834. }
  835. else {
  836. $result = $mongoCollection->remove($this->getCriteria(), array('justOne' => true, 'safe' => $safe));
  837. }
  838. // Execute post delete hook
  839. $this->postDelete();
  840. return $result;
  841. }
  842. /**
  843. * Get a property
  844. *
  845. * @param $property
  846. * @return mixed
  847. */
  848. public function __get($property)
  849. {
  850. return $this->getProperty($property);
  851. }
  852. /**
  853. * Set a property
  854. *
  855. * @param string $property
  856. * @param mixed $value
  857. */
  858. public function __set($property, $value)
  859. {
  860. return $this->setProperty($property, $value);
  861. }
  862. /**
  863. * Test to see if a property is set
  864. *
  865. * @param string $property
  866. */
  867. public function __isset($property)
  868. {
  869. return $this->hasProperty($property);
  870. }
  871. /**
  872. * Unset a property
  873. *
  874. * @param string $property
  875. */
  876. public function __unset($property)
  877. {
  878. $this->_data[$property] = null;
  879. }
  880. /**
  881. * Get an offset
  882. *
  883. * @param string $offset
  884. * @return mixed
  885. */
  886. public function offsetGet($offset)
  887. {
  888. return $this->getProperty($offset);
  889. }
  890. /**
  891. * set an offset
  892. *
  893. * @param string $offset
  894. * @param mixed $value
  895. */
  896. public function offsetSet($offset, $value)
  897. {
  898. return $this->setProperty($offset, $value);
  899. }
  900. /**
  901. * Test to see if an offset exists
  902. *
  903. * @param string $offset
  904. */
  905. public function offsetExists($offset)
  906. {
  907. return $this->hasProperty($offset);
  908. }
  909. /**
  910. * Unset a property
  911. *
  912. * @param string $offset
  913. */
  914. public function offsetUnset($offset)
  915. {
  916. $this->_data[$offset] = null;
  917. }
  918. /**
  919. * Count all properties in this document
  920. *
  921. * @return int
  922. */
  923. public function count()
  924. {
  925. return count($this->getPropertyKeys());
  926. }
  927. /**
  928. * Get the document iterator
  929. *
  930. * @return Shanty_Mongo_DocumentIterator
  931. */
  932. public function getIterator()
  933. {
  934. return new Shanty_Mongo_Iterator_Default($this);
  935. }
  936. /**
  937. * Get all operations
  938. *
  939. * @param Boolean $includingChildren Get operations from children as well
  940. */
  941. public function getOperations($includingChildren = false)
  942. {
  943. $operations = $this->_operations;
  944. if ($includingChildren) {
  945. foreach ($this->_data as $property => $document) {
  946. if (!($document instanceof Shanty_Mongo_Document)) continue;
  947. if (!$this->isReference($document) && !$this->hasRequirement($property, 'AsReference')) {
  948. $operations = array_merge_recursive($operations, $document->getOperations(true));
  949. }
  950. }
  951. }
  952. return $operations;
  953. }
  954. /**
  955. * Remove all operations
  956. *
  957. * @param Boolean $includingChildren Remove operations from children as wells
  958. */
  959. public function purgeOperations($includingChildren = false)
  960. {
  961. if ($includingChildren) {
  962. foreach ($this->_data as $property => $document) {
  963. if (!($document instanceof Shanty_Mongo_Document)) continue;
  964. if (!$this->isReference($document) || $this->hasRequirement($property, 'AsReference')) {
  965. $document->purgeOperations(true);
  966. }
  967. }
  968. }
  969. $this->_operations = array();
  970. }
  971. /**
  972. * Add an operation
  973. *
  974. * @param string $operation
  975. * @param array $data
  976. */
  977. public function addOperation($operation, $property = null, $value = null)
  978. {
  979. // Make sure the operation is valid
  980. if (!Shanty_Mongo::isValidOperation($operation)) {
  981. require_once 'Shanty/Mongo/Exception.php';
  982. throw new Shanty_Mongo_Exception("'{$operation}' is not valid operation");
  983. }
  984. // Prime the specific operation
  985. if (!array_key_exists($operation, $this->_operations)) {
  986. $this->_operations[$operation] = array();
  987. }
  988. // Save the operation
  989. if (is_null($property)) {
  990. $path = $this->getPathToDocument();
  991. }
  992. else {
  993. $path = $this->getPathToProperty($property);
  994. }
  995. // Mix operation with existing operations if needed
  996. switch($operation) {
  997. case '$pushAll':
  998. case '$pullAll':
  999. if (!array_key_exists($path, $this->_operations[$operation])) {
  1000. break;
  1001. }
  1002. $value = array_merge($this->_operations[$operation][$path], $value);
  1003. break;
  1004. }
  1005. $this->_operations[$operation][$path] = $value;
  1006. }
  1007. /**
  1008. * Increment a property by a specified amount
  1009. *
  1010. * @param string $property
  1011. * @param int $value
  1012. */
  1013. public function inc($property, $value = 1)
  1014. {
  1015. return $this->addOperation('$inc', $property, $value);
  1016. }
  1017. /**
  1018. * Push a value to a property
  1019. *
  1020. * @param string $property
  1021. * @param mixed $value
  1022. */
  1023. public function push($property = null, $value = null)
  1024. {
  1025. // Export value if needed
  1026. if ($value instanceof Shanty_Mongo_Document) {
  1027. $value = $value->export();
  1028. }
  1029. return $this->addOperation('$pushAll', $property, array($value));
  1030. }
  1031. /**
  1032. * Pull all occurrences a value from an array
  1033. *
  1034. * @param string $property
  1035. * @param mixed $value
  1036. */
  1037. public function pull($property, $value)
  1038. {
  1039. return $this->addOperation('$pullAll', $property, $value);
  1040. }
  1041. /*
  1042. * Adds value to the array only if its not in the array already.
  1043. *
  1044. * @param string $property
  1045. * @param mixed $value
  1046. */
  1047. public function addToSet($property, $value)
  1048. {
  1049. return $this->addOperation('$addToSet', $property, $value);
  1050. }
  1051. /*
  1052. * Removes an element from an array
  1053. *
  1054. * @param string $property
  1055. * @param mixed $value
  1056. */
  1057. public function pop($property, $value)
  1058. {
  1059. return $this->addOperation('$pop', $property, $value);
  1060. }
  1061. }