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