PageRenderTime 54ms CodeModel.GetById 25ms RepoModel.GetById 1ms app.codeStats 0ms

/library/Shanty/Mongo/Document.php

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