PageRenderTime 47ms CodeModel.GetById 14ms RepoModel.GetById 1ms app.codeStats 0ms

/library/Shanty/Mongo/Document.php

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