Document.php 33 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366
  1. <?php
  2. //require_once 'Thinkopen/Mongodb/Mongo/Exception.php';
  3. //require_once 'Thinkopen/Mongodb/Mongo/Collection.php';
  4. //require_once 'Thinkopen/Mongodb/Mongo/Iterator/Default.php';
  5. /**
  6. * @category Thinkopen
  7. * @package Mooses_Mongodb_Mongo
  8. * @copyright Thinkopen s.r.l.
  9. * @license New BSD License
  10. */
  11. class Mooses_Mongodb_Mongo_Document extends Mooses_Mongodb_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. Mooses_Mongodb_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. throw new Mooses_Mongodb_Mongo_Exception('Can not fetch instance of MongoDb. Document is not connected to a db.');
  276. }
  277. if ($writable) $connection = Mooses_Mongodb_Mongo::getWriteConnection($this->getConfigAttribute('connectionGroup'));
  278. else $connection = Mooses_Mongodb_Mongo::getReadConnection($this->getConfigAttribute('connectionGroup'));
  279. $temp = $connection->selectDB($this->getConfigAttribute('db'));
  280. # Tells replica set how many nodes must have the data before success
  281. // $temp->w = 2;
  282. return $temp;
  283. }
  284. /**
  285. * Fetch an instance of MongoCollection
  286. *
  287. * @param boolean $writable
  288. * @return MongoCollection
  289. */
  290. public function _getMongoCollection($writable = true)
  291. {
  292. if (is_null($this->getConfigAttribute('collection'))) {
  293. throw new Mooses_Mongodb_Mongo_Exception('Can not fetch instance of MongoCollection. Document is not connected to a collection.');
  294. }
  295. return $this->_getMongoDb($writable)->selectCollection($this->getConfigAttribute('collection'));
  296. }
  297. /**
  298. * Apply a set of requirements
  299. *
  300. * @param array $requirements
  301. */
  302. public function applyRequirements($requirements, $dirty = true)
  303. {
  304. if ($dirty) {
  305. $requirements = static::makeRequirementsTidy($requirements);
  306. }
  307. $this->_docRequirements = static::mergeRequirements($this->_docRequirements, $requirements);
  308. $this->_filters = null;
  309. $this->_validators = null;
  310. }
  311. /**
  312. * Test if this document has a particular requirement
  313. *
  314. * @param string $property
  315. * @param string $requirement
  316. */
  317. public function hasRequirement($property, $requirement)
  318. {
  319. if (!array_key_exists($property, $this->_docRequirements)) return false;
  320. switch($requirement) {
  321. case 'Document':
  322. case 'DocumentSet':
  323. foreach ($this->_docRequirements[$property] as $requirementSearch => $params) {
  324. $standardClass = 'Mooses_Mongodb_Mongo_'.$requirement;
  325. // Return basic document or document set class if requirement matches
  326. if ($requirementSearch == $requirement) {
  327. return $standardClass;
  328. }
  329. // Find the document class
  330. $matches = array();
  331. preg_match("/^{$requirement}:([A-Za-z][\w\-]*)$/", $requirementSearch, $matches);
  332. if (!empty($matches)) {
  333. if (!class_exists($matches[1])) {
  334. throw new Mooses_Mongodb_Mongo_Exception("$requirement class of '{$matches[1]}' does not exist");
  335. }
  336. if (!is_subclass_of($matches[1], $standardClass)) {
  337. throw new Mooses_Mongodb_Mongo_Exception("$requirement of '{$matches[1]}' sub is not a class of $standardClass does not exist");
  338. }
  339. return $matches[1];
  340. }
  341. }
  342. return false;
  343. }
  344. return array_key_exists($requirement, $this->_docRequirements[$property]);
  345. }
  346. /**
  347. * Get all requirements. If prefix is provided then only the requirements for
  348. * the properties that start with prefix will be returned.
  349. *
  350. * @param string $prefix
  351. */
  352. public function getRequirements($prefix = null)
  353. {
  354. // If no prefix is provided return all requirements
  355. if (is_null($prefix)) return $this->_docRequirements;
  356. // Find requirements for all properties starting with prefix
  357. $properties = array_filter(array_keys($this->_docRequirements), function($value) use ($prefix) {
  358. return (substr_compare($value, $prefix, 0, strlen($prefix)) == 0 && strlen($value) > strlen($prefix));
  359. });
  360. $requirements = array_intersect_key($this->_docRequirements, array_flip($properties));
  361. // Remove prefix from requirement key
  362. $newRequirements = array();
  363. array_walk($requirements, function($value, $key) use ($prefix, &$newRequirements) {
  364. $newRequirements[substr($key, strlen($prefix))] = $value;
  365. });
  366. return $newRequirements;
  367. }
  368. /**
  369. * Add a requirement to a property
  370. *
  371. * @param string $property
  372. * @param string $requirement
  373. */
  374. public function addRequirement($property, $requirement, $options = null)
  375. {
  376. if (!array_key_exists($property, $this->_docRequirements)) {
  377. $this->_docRequirements[$property] = array();
  378. }
  379. $this->_docRequirements[$property][$requirement] = $options;
  380. unset($this->_filters[$property]);
  381. unset($this->_validators[$property]);
  382. }
  383. /**
  384. * Remove a requirement from a property
  385. *
  386. * @param string $property
  387. * @param string $requirement
  388. */
  389. public function removeRequirement($property, $requirement)
  390. {
  391. if (!array_key_exists($property, $this->_docRequirements)) return;
  392. foreach ($this->_docRequirements[$property] as $requirementItem => $options) {
  393. if ($requirement === $requirementItem) {
  394. unset($this->_docRequirements[$property][$requirementItem]);
  395. unset($this->_filters[$property]);
  396. unset($this->_validators[$property]);
  397. }
  398. }
  399. }
  400. /**
  401. * Get all the properties with a particular requirement
  402. *
  403. * @param array $requirement
  404. */
  405. public function getPropertiesWithRequirement($requirement)
  406. {
  407. $properties = array();
  408. foreach ($this->_docRequirements as $property => $requirementList) {
  409. if (strpos($property, '.') > 0) continue;
  410. if (array_key_exists($requirement, $requirementList)) {
  411. $properties[] = $property;
  412. }
  413. }
  414. return $properties;
  415. }
  416. /**
  417. * Load the requirements as validators or filters for a given property,
  418. * and cache them as validators or filters, respectively.
  419. *
  420. * @param String $property Name of property
  421. * @return boolean whether or not cache was used.
  422. */
  423. public function loadRequirements($property)
  424. {
  425. if (isset($this->_validators[$property]) || isset($this->_filters[$property])) {
  426. return true;
  427. }
  428. $validators = new Zend_Validate;
  429. $filters = new Zend_Filter;
  430. if (!isset($this->_docRequirements[$property])) {
  431. $this->_filters[$property] = $filters;
  432. $this->_validators[$property] = $validators;
  433. return false;
  434. }
  435. foreach ($this->_docRequirements[$property] as $requirement => $options) {
  436. $req = Mooses_Mongodb_Mongo::retrieveRequirement($requirement, $options);
  437. if ($req instanceof Zend_Validate_Interface) {
  438. $validators->addValidator($req);
  439. } else if ($req instanceof Zend_Filter_Interface) {
  440. $filters->addFilter($req);
  441. }
  442. }
  443. $this->_filters[$property] = $filters;
  444. $this->_validators[$property] = $validators;
  445. return false;
  446. }
  447. /**
  448. * Get all validators attached to a property
  449. *
  450. * @param String $property Name of property
  451. * @return Zend_Validate
  452. **/
  453. public function getValidators($property)
  454. {
  455. $this->loadRequirements($property);
  456. return $this->_validators[$property];
  457. }
  458. /**
  459. * Get all filters attached to a property
  460. *
  461. * @param String $property
  462. * @return Zend_Filter
  463. */
  464. public function getFilters($property)
  465. {
  466. $this->loadRequirements($property);
  467. return $this->_filters[$property];
  468. }
  469. /**
  470. * Test if a value is valid against a property
  471. *
  472. * @param String $property
  473. * @param Boolean $value
  474. */
  475. public function isValid($property, $value)
  476. {
  477. $validators = $this->getValidators($property);
  478. return $validators->isValid($value);
  479. }
  480. public function getAllData(){
  481. return array_diff_key($this->_cleanData, array("_type" => NULL));
  482. }
  483. public function getKeys(){
  484. return array_keys($this->_cleanData);
  485. }
  486. /**
  487. * Get a property
  488. *
  489. * @param mixed $property
  490. */
  491. public function getProperty($property)
  492. {
  493. // If property exists and initialised then return it
  494. if (array_key_exists($property, $this->_data)) {
  495. return $this->_data[$property];
  496. }
  497. // Fetch clean data for this property
  498. if (array_key_exists($property, $this->_cleanData)) {
  499. $data = $this->_cleanData[$property];
  500. }
  501. else {
  502. $data = array();
  503. }
  504. // If data is not an array then we can do nothing else with it
  505. if (!is_array($data)) {
  506. $this->_data[$property] = $data;
  507. return $this->_data[$property];
  508. }
  509. // If property is supposed to be an array then initialise an array
  510. if ($this->hasRequirement($property, 'Array')) {
  511. return $this->_data[$property] = $data;
  512. }
  513. // If property is a reference to another document then fetch the reference document
  514. $db = $this->getConfigAttribute('db');
  515. if (MongoDBRef::isRef($data)) {
  516. $collection = $data['$ref'];
  517. $data = MongoDBRef::get($this->_getMongoDB(false), $data);
  518. // If this is a broken reference then no point keeping it for later
  519. if (!$data) {
  520. $this->_data[$property] = null;
  521. return $this->_data[$property];
  522. }
  523. $reference = true;
  524. }
  525. else {
  526. $collection = $this->getConfigAttribute('collection');
  527. $reference = false;
  528. }
  529. // Find out the class name of the document or document set we are loaded
  530. if ($className = $this->hasRequirement($property, 'DocumentSet')) {
  531. $docType = 'Mooses_Mongodb_Mongo_DocumentSet';
  532. }
  533. else {
  534. $className = $this->hasRequirement($property, 'Document');
  535. // Load a document anyway so long as $data is not empty
  536. if (!$className && !empty($data)) {
  537. $className = 'Mooses_Mongodb_Mongo_Document';
  538. }
  539. if ($className) $docType = 'Mooses_Mongodb_Mongo_Document';
  540. }
  541. // Nothing else to do
  542. if (!$className) return null;
  543. // Configure property for document/documentSet usage
  544. $config = array();
  545. $config['new'] = empty($data);
  546. $config['connectionGroup'] = $this->getConfigAttribute('connectionGroup');
  547. $config['db'] = $this->getConfigAttribute('db');
  548. $config['collection'] = $collection;
  549. $config['requirementModifiers'] = $this->getRequirements($property.'.');
  550. $config['hasId'] = $this->hasRequirement($property, 'hasId');
  551. if (!$reference) {
  552. $config['pathToDocument'] = $this->getPathToProperty($property);
  553. $config['criteria'] = $this->getCriteria();
  554. }
  555. // Initialise document
  556. $document = new $className($data, $config);
  557. // if this document was a reference then remember that
  558. if ($reference) {
  559. $this->_references->attach($document);
  560. }
  561. $this->_data[$property] = $document;
  562. return $this->_data[$property];
  563. }
  564. /**
  565. * Set a property
  566. *
  567. * @param mixed $property
  568. * @param mixed $value
  569. */
  570. public function setProperty($property, $value, $_forceCleanData = false)
  571. {
  572. $validators = $this->getValidators($property);
  573. // Throw exception if value is not valid
  574. if (!is_null($value) && !$validators->isValid($value)) {
  575. require_once 'Thinkopen/Mongdb/Mongo/Exception.php';
  576. throw new Mooses_Mongodb_Mongo_Exception(implode($validators->getMessages(), "\n"));
  577. }
  578. // Unset property
  579. if (is_null($value)) {
  580. $this->_data[$property] = null;
  581. return;
  582. }
  583. if ($value instanceof Mooses_Mongodb_Mongo_Document && !$this->hasRequirement($property, 'AsReference')) {
  584. if (!$value->isNewDocument() || !$value->isRootDocument()) {
  585. $documentClass = get_class($value);
  586. $value = new $documentClass($value->export(), array('new' => false, 'pathToDocument' => $this->getPathToProperty($property)));
  587. }
  588. else {
  589. $value->setPathToDocument($this->getPathToProperty($property));
  590. }
  591. $value->setConfigAttribute('connectionGroup', $this->getConfigAttribute('connectionGroup'));
  592. $value->setConfigAttribute('db', $this->getConfigAttribute('db'));
  593. $value->setConfigAttribute('collection', $this->getConfigAttribute('collection'));
  594. $value->setConfigAttribute('criteria', $this->getCriteria());
  595. $value->applyRequirements($this->getRequirements($property.'.'));
  596. }
  597. // Filter value
  598. $value = $this->getFilters($property)->filter($value);
  599. if($_forceCleanData) {
  600. $this->_cleanData[$property] = $value;
  601. }
  602. $this->_data[$property] = $value;
  603. }
  604. /**
  605. * Determine if this document has a property
  606. *
  607. * @param $property
  608. * @return boolean
  609. */
  610. public function hasProperty($property)
  611. {
  612. // If property has been initialised
  613. if (array_key_exists($property, $this->_data)) {
  614. return !is_null($this->_data[$property]);
  615. }
  616. // If property has not been initialised
  617. if (array_key_exists($property, $this->_cleanData)) {
  618. return !is_null($this->_cleanData[$property]);
  619. }
  620. return false;
  621. }
  622. /**
  623. * Get a list of all property keys in this document
  624. */
  625. public function getPropertyKeys()
  626. {
  627. $keyList = array();
  628. $doNoCount = array();
  629. foreach ($this->_data as $property => $value) {
  630. if (($value instanceof Mooses_Mongodb_Mongo_Document && !$value->isEmpty()) ||
  631. (!($value instanceof Mooses_Mongodb_Mongo_Document) && !is_null($value))) {
  632. $keyList[] = $property;
  633. }
  634. else {
  635. $doNoCount[] = $property;
  636. }
  637. }
  638. foreach ($this->_cleanData as $property => $value) {
  639. if (in_array($property, $keyList, true) || in_array($property, $doNoCount, true)) continue;
  640. if (!is_null($value)) $keyList[] = $property;
  641. }
  642. return $keyList;
  643. }
  644. /**
  645. * Create a reference to this document
  646. *
  647. * @return array
  648. */
  649. public function createReference()
  650. {
  651. if (!$this->isRootDocument()) {
  652. require_once 'Thinkopen/Mongdb/Mongo/Exception.php';
  653. throw new Mooses_Mongodb_Mongo_Exception('Can not create reference. Document is not a root document');
  654. }
  655. if (!$this->isConnected()) {
  656. require_once 'Thinkopen/Mongdb/Mongo/Exception.php';
  657. throw new Mooses_Mongodb_Mongo_Exception('Can not create reference. Document does not connected to a db and collection');
  658. }
  659. return MongoDBRef::create($this->getConfigAttribute('collection'), $this->getId());
  660. }
  661. /**
  662. * Test to see if a document is a reference in this document
  663. *
  664. * @param Mooses_Mongodb_Mongo_Document $document
  665. * @return boolean
  666. */
  667. public function isReference(Mooses_Mongodb_Mongo_Document $document)
  668. {
  669. return $this->_references->contains($document);
  670. }
  671. /**
  672. * Determine if the document has a given reference or not
  673. *
  674. * @Return Boolean
  675. */
  676. public function hasReference($referenceName)
  677. {
  678. return !is_null($this->getProperty($referenceName));
  679. }
  680. /**
  681. * Export all data
  682. *
  683. * @return array
  684. */
  685. public function export($skipRequired = false)
  686. {
  687. $exportData = $this->_cleanData;
  688. foreach ($this->_data as $property => $value) {
  689. // If property has been deleted
  690. if (is_null($value)) {
  691. unset($exportData[$property]);
  692. continue;
  693. }
  694. // If property is a document
  695. if ($value instanceof Mooses_Mongodb_Mongo_Document) {
  696. // Make when exporting from a documentset look up the correct requirement index
  697. if ($this instanceof Mooses_Mongodb_Mongo_DocumentSet) {
  698. $requirementIndex = Mooses_Mongodb_Mongo_DocumentSet::DYNAMIC_INDEX;
  699. }
  700. else {
  701. $requirementIndex = $property;
  702. }
  703. // If document is supposed to be a reference
  704. if ($this->hasRequirement($requirementIndex, 'AsReference') || $this->isReference($value)) {
  705. $exportData[$property] = $value->createReference();
  706. continue;
  707. }
  708. $data = $value->export();
  709. if (!empty($data)) {
  710. $exportData[$property] = $data;
  711. }
  712. continue;
  713. }
  714. $exportData[$property] = $value;
  715. }
  716. if (!$skipRequired) {
  717. // make sure required properties are not empty
  718. $requiredProperties = $this->getPropertiesWithRequirement('Required');
  719. foreach ($requiredProperties as $property) {
  720. if (!isset($exportData[$property]) || (is_array($exportData[$property]) && empty($exportData[$property]))) {
  721. require_once 'Thinkopen/Mongdb/Mongo/Exception.php';
  722. throw new Mooses_Mongodb_Mongo_Exception("Property '{$property}' must not be null.");
  723. }
  724. }
  725. }
  726. return $exportData;
  727. }
  728. /**
  729. * Is this document a new document
  730. *
  731. * @return boolean
  732. */
  733. public function isNewDocument()
  734. {
  735. return ($this->_config['new']);
  736. }
  737. /**
  738. * Test to see if this document is empty
  739. *
  740. * @return Boolean
  741. */
  742. public function isEmpty()
  743. {
  744. $doNoCount = array();
  745. foreach ($this->_data as $property => $value) {
  746. if ($value instanceof Mooses_Mongodb_Mongo_Document) {
  747. if (!$value->isEmpty()) return false;
  748. }
  749. elseif (!is_null($value)) {
  750. return false;
  751. }
  752. $doNoCount[] = $property;
  753. }
  754. foreach ($this->_cleanData as $property => $value) {
  755. if (in_array($property, $doNoCount)) {
  756. continue;
  757. }
  758. if (!is_null($value)) {
  759. return false;
  760. }
  761. }
  762. return true;
  763. }
  764. /**
  765. * Convert data changes into operations
  766. *
  767. * @param array $data
  768. */
  769. public function processChanges(array $data = array())
  770. {
  771. foreach ($data as $property => $value) {
  772. if ($property === '_id') continue;
  773. if (!array_key_exists($property, $this->_cleanData)) {
  774. $this->addOperation('$set', $property, $value);
  775. continue;
  776. }
  777. $newValue = $value;
  778. $oldValue = $this->_cleanData[$property];
  779. if (MongoDBRef::isRef($newValue) && MongoDBRef::isRef($oldValue)) {
  780. $newValue['$id'] = $newValue['$id']->__toString();
  781. $oldValue['$id'] = $oldValue['$id']->__toString();
  782. }
  783. if ($newValue !== $oldValue) {
  784. $this->addOperation('$set', $property, $value);
  785. }
  786. }
  787. foreach ($this->_cleanData as $property => $value) {
  788. if (array_key_exists($property, $data)) continue;
  789. $this->addOperation('$unset', $property, 1);
  790. }
  791. }
  792. /**
  793. * Removes any properties that have been flagged as ignore in properties.
  794. *
  795. * @return void
  796. * @author Tom Holder
  797. **/
  798. public function removeIgnoredProperties(&$exportData)
  799. {
  800. // remove ignored properties
  801. $ignoreProperties = $this->getPropertiesWithRequirement('Ignore');
  802. foreach ($this->_data as $property => $document) {
  803. if (!($document instanceof Mooses_Mongodb_Mongo_Document)) continue;
  804. if ($this->isReference($document) || $this->hasRequirement($property, 'AsReference')) continue;
  805. $document->removeIgnoredProperties($exportData[$property]);
  806. }
  807. foreach ($ignoreProperties as $property) {
  808. unset($exportData[$property]);
  809. }
  810. }
  811. /**
  812. * Save this document
  813. *
  814. * @param boolean $entierDocument Force the saving of the entier document, instead of just the changes
  815. * @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
  816. * @return boolean Result of save
  817. */
  818. public function save($entierDocument = false, $safe = true)
  819. {
  820. if (!$this->isConnected()) {
  821. require_once 'Thinkopen/Mongdb/Mongo/Exception.php';
  822. throw new Mooses_Mongodb_Mongo_Exception('Can not save documet. Document is not connected to a db and collection');
  823. }
  824. if ($this->isLocked()) {
  825. require_once 'Thinkopen/Mongdb/Mongo/Exception.php';
  826. throw new Mooses_Mongodb_Mongo_Exception('Can not save documet. Document is locked.');
  827. }
  828. ## execute pre hooks
  829. if ($this->isNewDocument()) $this->preInsert();
  830. else $this->preUpdate();
  831. $this->preSave();
  832. $exportData = $this->export();
  833. //Remove data with Ignore requirement.
  834. $this->removeIgnoredProperties($exportData);
  835. if ($this->isRootDocument() && ($this->isNewDocument() || $entierDocument)) {
  836. // Save the entier document
  837. $operations = $exportData;
  838. }
  839. else {
  840. // Update an existing document and only send the changes
  841. if (!$this->isRootDocument()) {
  842. // are we updating a child of an array?
  843. if ($this->isNewDocument() && $this->isParentDocumentSet()) {
  844. $this->_operations['$push'][$this->getPathToDocument()] = $exportData;
  845. $exportData = array();
  846. /**
  847. * 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.
  848. * Locking prevents overriding the parent array on another save() after this save().
  849. */
  850. $this->setConfigAttribute('locked', true);
  851. }
  852. }
  853. // Convert all data changes into sets and unsets
  854. $this->processChanges($exportData);
  855. $operations = $this->getOperations(true);
  856. // There are no changes, return so we don't blank the object
  857. if (empty($operations)) {
  858. return true;
  859. }
  860. }
  861. $result = false;
  862. if($this->isNewDocument())
  863. {
  864. $result = $this->_getMongoCollection(true)->update($this->getCriteria(), $operations, array('upsert' => true, 'safe' => $safe));
  865. $this->_cleanData = $exportData;
  866. }
  867. else
  868. {
  869. $newversion = $this->_getMongoDb(true)->command(
  870. array(
  871. 'findandmodify' => $this->getConfigAttribute('collection'),
  872. 'query' => $this->getCriteria(),
  873. 'update'=>$operations,
  874. 'new'=>true )
  875. );
  876. if(isset($newversion['value']))
  877. $this->_cleanData = $newversion['value'];
  878. if($newversion['ok'] == 1)
  879. $result = true;
  880. }
  881. $this->_data = array();
  882. $this->purgeOperations(true);
  883. // Run post hooks
  884. if ($this->isNewDocument()) {
  885. // This is not a new document anymore
  886. $this->setConfigAttribute('new', false);
  887. $this->postInsert();
  888. }
  889. else {
  890. $this->postUpdate();
  891. }
  892. $this->postSave();
  893. return $result;
  894. }
  895. /**
  896. * Save this document without waiting for a response from the server
  897. *
  898. * @param boolean $entierDocument Force the saving of the entier document, instead of just the changes
  899. * @return boolean Result of save
  900. */
  901. public function saveUnsafe($entierDocument = false)
  902. {
  903. return $this->save($entierDocument, false);
  904. }
  905. /**
  906. * Delete this document
  907. *
  908. * $return boolean Result of delete
  909. */
  910. public function delete($safe = true)
  911. {
  912. if (!$this->isConnected()) {
  913. require_once 'Thinkopen/Mongdb/Mongo/Exception.php';
  914. throw new Mooses_Mongodb_Mongo_Exception('Can not delete document. Document is not connected to a db and collection');
  915. }
  916. if ($this->isLocked()) {
  917. require_once 'Thinkopen/Mongdb/Mongo/Exception.php';
  918. throw new Mooses_Mongodb_Mongo_Exception('Can not save documet. Document is locked.');
  919. }
  920. $mongoCollection = $this->_getMongoCollection(true);
  921. // Execute pre delete hook
  922. $this->preDelete();
  923. if (!$this->isRootDocument()) {
  924. $result = $mongoCollection->update($this->getCriteria(), array('$unset' => array($this->getPathToDocument() => 1)), array('safe' => $safe));
  925. }
  926. else {
  927. $result = $mongoCollection->remove($this->getCriteria(), array('justOne' => true, 'safe' => $safe));
  928. }
  929. // Execute post delete hook
  930. $this->postDelete();
  931. return $result;
  932. }
  933. /**
  934. * Get a property
  935. *
  936. * @param $property
  937. * @return mixed
  938. */
  939. public function __get($property)
  940. {
  941. return $this->getProperty($property);
  942. }
  943. /**
  944. * Set a property
  945. *
  946. * @param string $property
  947. * @param mixed $value
  948. */
  949. public function __set($property, $value)
  950. {
  951. return $this->setProperty($property, $value);
  952. }
  953. /**
  954. * Test to see if a property is set
  955. *
  956. * @param string $property
  957. */
  958. public function __isset($property)
  959. {
  960. return $this->hasProperty($property);
  961. }
  962. /**
  963. * Unset a property
  964. *
  965. * @param string $property
  966. */
  967. public function __unset($property)
  968. {
  969. $this->_data[$property] = null;
  970. }
  971. /**
  972. * Get an offset
  973. *
  974. * @param string $offset
  975. * @return mixed
  976. */
  977. public function offsetGet($offset)
  978. {
  979. return $this->getProperty($offset);
  980. }
  981. /**
  982. * set an offset
  983. *
  984. * @param string $offset
  985. * @param mixed $value
  986. */
  987. public function offsetSet($offset, $value)
  988. {
  989. return $this->setProperty($offset, $value);
  990. }
  991. /**
  992. * Test to see if an offset exists
  993. *
  994. * @param string $offset
  995. */
  996. public function offsetExists($offset)
  997. {
  998. return $this->hasProperty($offset);
  999. }
  1000. /**
  1001. * Unset a property
  1002. *
  1003. * @param string $offset
  1004. */
  1005. public function offsetUnset($offset)
  1006. {
  1007. $this->_data[$offset] = null;
  1008. }
  1009. /**
  1010. * Count all properties in this document
  1011. *
  1012. * @return int
  1013. */
  1014. public function count()
  1015. {
  1016. return count($this->getPropertyKeys());
  1017. }
  1018. /**
  1019. * Get the document iterator
  1020. *
  1021. * @return Mooses_Mongodb_Mongo_DocumentIterator
  1022. */
  1023. public function getIterator()
  1024. {
  1025. return new Mooses_Mongodb_Mongo_Iterator_Default($this);
  1026. }
  1027. /**
  1028. * Get all operations
  1029. *
  1030. * @param Boolean $includingChildren Get operations from children as well
  1031. */
  1032. public function getOperations($includingChildren = false)
  1033. {
  1034. $operations = $this->_operations;
  1035. if ($includingChildren) {
  1036. foreach ($this->_data as $property => $document) {
  1037. if (!($document instanceof Mooses_Mongodb_Mongo_Document)) continue;
  1038. if (!$this->isReference($document) && !$this->hasRequirement($property, 'AsReference')) {
  1039. $operations = array_merge_recursive($operations, $document->getOperations(true));
  1040. }
  1041. }
  1042. }
  1043. return $operations;
  1044. }
  1045. /**
  1046. * Remove all operations
  1047. *
  1048. * @param Boolean $includingChildren Remove operations from children as wells
  1049. */
  1050. public function purgeOperations($includingChildren = false)
  1051. {
  1052. if ($includingChildren) {
  1053. foreach ($this->_data as $property => $document) {
  1054. if (!($document instanceof Mooses_Mongodb_Mongo_Document)) continue;
  1055. if (!$this->isReference($document) || $this->hasRequirement($property, 'AsReference')) {
  1056. $document->purgeOperations(true);
  1057. }
  1058. }
  1059. }
  1060. $this->_operations = array();
  1061. }
  1062. /**
  1063. * Add an operation
  1064. *
  1065. * @param string $operation
  1066. * @param array $data
  1067. */
  1068. public function addOperation($operation, $property = null, $value = null)
  1069. {
  1070. // Make sure the operation is valid
  1071. if (!Mooses_Mongodb_Mongo::isValidOperation($operation)) {
  1072. require_once 'Thinkopen/Mongdb/Mongo/Exception.php';
  1073. throw new Mooses_Mongodb_Mongo_Exception("'{$operation}' is not valid operation");
  1074. }
  1075. // Prime the specific operation
  1076. if (!array_key_exists($operation, $this->_operations)) {
  1077. $this->_operations[$operation] = array();
  1078. }
  1079. // Save the operation
  1080. if (is_null($property)) {
  1081. $path = $this->getPathToDocument();
  1082. }
  1083. else {
  1084. $path = $this->getPathToProperty($property);
  1085. }
  1086. // Mix operation with existing operations if needed
  1087. switch($operation) {
  1088. case '$pushAll':
  1089. case '$pullAll':
  1090. if (!array_key_exists($path, $this->_operations[$operation])) {
  1091. break;
  1092. }
  1093. $value = array_merge($this->_operations[$operation][$path], $value);
  1094. break;
  1095. }
  1096. $this->_operations[$operation][$path] = $value;
  1097. }
  1098. /**
  1099. * Increment a property by a specified amount
  1100. *
  1101. * @param string $property
  1102. * @param int $value
  1103. */
  1104. public function inc($property, $value = 1)
  1105. {
  1106. return $this->addOperation('$inc', $property, $value);
  1107. }
  1108. /**
  1109. * Push a value to a property
  1110. *
  1111. * @param string $property
  1112. * @param mixed $value
  1113. */
  1114. public function push($property = null, $value = null)
  1115. {
  1116. // Export value if needed
  1117. if ($value instanceof Mooses_Mongodb_Mongo_Document) {
  1118. $value = $value->export();
  1119. }
  1120. return $this->addOperation('$pushAll', $property, array($value));
  1121. }
  1122. /**
  1123. * Pull all occurrences a value from an array
  1124. *
  1125. * @param string $property
  1126. * @param mixed $value
  1127. */
  1128. public function pull($property, $value)
  1129. {
  1130. return $this->addOperation('$pullAll', $property, $value);
  1131. }
  1132. /*
  1133. * Adds value to the array only if its not in the array already.
  1134. *
  1135. * @param string $property
  1136. * @param mixed $value
  1137. */
  1138. public function addToSet($property, $value)
  1139. {
  1140. return $this->addOperation('$addToSet', $property, $value);
  1141. }
  1142. /*
  1143. * Removes an element from an array
  1144. *
  1145. * @param string $property
  1146. * @param mixed $value
  1147. */
  1148. public function pop($property, $value)
  1149. {
  1150. return $this->addOperation('$pop', $property, $value);
  1151. }
  1152. }