- Fixed events error after save reported by @ibolmo; Added some tests for this bug
[activemongo.git] / lib / ActiveMongo.php
bloba3b923ae5bcb57f6cbec7f74ea35d6ebe655f34d
1 <?php
2 /*
3 +---------------------------------------------------------------------------------+
4 | Copyright (c) 2010 ActiveMongo |
5 +---------------------------------------------------------------------------------+
6 | Redistribution and use in source and binary forms, with or without |
7 | modification, are permitted provided that the following conditions are met: |
8 | 1. Redistributions of source code must retain the above copyright |
9 | notice, this list of conditions and the following disclaimer. |
10 | |
11 | 2. Redistributions in binary form must reproduce the above copyright |
12 | notice, this list of conditions and the following disclaimer in the |
13 | documentation and/or other materials provided with the distribution. |
14 | |
15 | 3. All advertising materials mentioning features or use of this software |
16 | must display the following acknowledgement: |
17 | This product includes software developed by César D. Rodas. |
18 | |
19 | 4. Neither the name of the César D. Rodas nor the |
20 | names of its contributors may be used to endorse or promote products |
21 | derived from this software without specific prior written permission. |
22 | |
23 | THIS SOFTWARE IS PROVIDED BY CÉSAR D. RODAS ''AS IS'' AND ANY |
24 | EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED |
25 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE |
26 | DISCLAIMED. IN NO EVENT SHALL CÉSAR D. RODAS BE LIABLE FOR ANY |
27 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES |
28 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; |
29 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND |
30 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
31 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS |
32 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE |
33 +---------------------------------------------------------------------------------+
34 | Authors: César Rodas <crodas@php.net> |
35 +---------------------------------------------------------------------------------+
38 // array get_document_vars(stdobj $obj) {{{
39 /**
40 * Simple hack to avoid get private and protected variables
42 * @param object $obj
43 * @param bool $include_id
45 * @return array
47 function get_document_vars($obj, $include_id=true)
49 $document = get_object_vars($obj);
50 if ($include_id && $obj->getID()) {
51 $document['_id'] = $obj->getID();
53 return $document;
55 // }}}
57 /**
58 * ActiveMongo
60 * Simple ActiveRecord pattern built on top of MongoDB. This class
61 * aims to provide easy iteration, data validation before update,
62 * and efficient update.
64 * @author César D. Rodas <crodas@php.net>
65 * @license PHP License
66 * @package ActiveMongo
67 * @version 1.0
70 abstract class ActiveMongo implements Iterator, Countable, ArrayAccess
73 // properties {{{
74 /**
75 * Current databases objects
77 * @type array
79 private static $_dbs;
80 /**
81 * Current collections objects
83 * @type array
85 private static $_collections;
86 /**
87 * Current connection to MongoDB
89 * @type MongoConnection
91 private static $_conn;
92 /**
93 * Database name
95 * @type string
97 private static $_db;
98 /**
99 * List of events handlers
101 * @type array
103 static private $_events = array();
105 * List of global events handlers
107 * @type array
109 static private $_super_events = array();
111 * Host name
113 * @type string
115 private static $_host;
117 * Current document
119 * @type array
121 private $_current = array();
123 * Result cursor
125 * @type MongoCursor
127 private $_cursor = null;
129 /* {{{ Silly but useful query abstraction */
130 private $_query = null;
131 private $_sort = null;
132 private $_limit = 0;
133 private $_skip = 0;
134 private $_properties = null;
135 /* }}} */
138 * Current document ID
140 * @type MongoID
142 private $_id;
145 * Tell if the current object
146 * is cloned or not.
148 * @type bool
150 private $_cloned = false;
151 // }}}
153 // GET CONNECTION CONFIG {{{
155 // string getCollectionName() {{{
157 * Get Collection Name, by default the class name,
158 * but you it can be override at the class itself to give
159 * a custom name.
161 * @return string Collection Name
163 protected function getCollectionName()
165 return strtolower(get_class($this));
167 // }}}
169 // string getDatabaseName() {{{
171 * Get Database Name, by default it is used
172 * the db name set by ActiveMong::connect()
174 * @return string DB Name
176 protected function getDatabaseName()
178 if (is_null(self::$_db)) {
179 throw new MongoException("There is no information about the default DB name");
181 return self::$_db;
183 // }}}
185 // void install() {{{
187 * Install.
189 * This static method iterate over the classes lists,
190 * and execute the setup() method on every ActiveMongo
191 * subclass. You should do this just once.
194 final public static function install()
196 $classes = array_reverse(get_declared_classes());
197 foreach ($classes as $class)
199 if ($class == __CLASS__) {
200 break;
202 if (is_subclass_of($class, __CLASS__)) {
203 $obj = new $class;
204 $obj->setup();
208 // }}}
210 // void connection($db, $host) {{{
212 * Connect
214 * This method setup parameters to connect to a MongoDB
215 * database. The connection is done when it is needed.
217 * @param string $db Database name
218 * @param string $host Host to connect
220 * @return void
222 final public static function connect($db, $host='localhost')
224 self::$_host = $host;
225 self::$_db = $db;
227 // }}}
229 // MongoConnection _getConnection() {{{
231 * Get Connection
233 * Get a valid database connection
235 * @return MongoConnection
237 final protected function _getConnection()
239 if (is_null(self::$_conn)) {
240 if (is_null(self::$_host)) {
241 self::$_host = 'localhost';
243 self::$_conn = new Mongo(self::$_host);
245 $dbname = $this->getDatabaseName();
246 if (!isSet(self::$_dbs[$dbname])) {
247 self::$_dbs[$dbname] = self::$_conn->selectDB($dbname);
249 return self::$_dbs[$dbname];
251 // }}}
253 // MongoCollection _getCollection() {{{
255 * Get Collection
257 * Get a collection connection.
259 * @return MongoCollection
261 final protected function _getCollection()
263 $colName = $this->getCollectionName();
264 if (!isset(self::$_collections[$colName])) {
265 self::$_collections[$colName] = self::_getConnection()->selectCollection($colName);
267 return self::$_collections[$colName];
269 // }}}
271 // }}}
273 // GET DOCUMENT TO SAVE OR UPDATE {{{
275 // bool getCurrentSubDocument(array &$document, string $parent_key, array $values, array $past_values) {{{
277 * Generate Sub-document
279 * This method build the difference between the current sub-document,
280 * and the origin one. If there is no difference, it would do nothing,
281 * otherwise it would build a document containing the differences.
283 * @param array &$document Document target
284 * @param string $parent_key Parent key name
285 * @param array $values Current values
286 * @param array $past_values Original values
288 * @return false
290 final function getCurrentSubDocument(&$document, $parent_key, Array $values, Array $past_values)
293 * The current property is a embedded-document,
294 * now we're looking for differences with the
295 * previous value (because we're on an update).
297 * It behaves exactly as getCurrentDocument,
298 * but this is simples (it doesn't support
299 * yet filters)
301 foreach ($values as $key => $value) {
302 $super_key = "{$parent_key}.{$key}";
303 if (is_array($value)) {
305 * Inner document detected
307 if (!isset($past_values[$key]) || !is_array($past_values[$key])) {
309 * We're lucky, it is a new sub-document,
310 * we simple add it
312 $document['$set'][$super_key] = $value;
313 } else {
315 * This is a document like this, we need
316 * to find out the differences to avoid
317 * network overhead.
319 if (!$this->getCurrentSubDocument($document, $super_key, $value, $past_values[$key])) {
320 return false;
323 continue;
324 } else if (!isset($past_values[$key]) || $past_values[$key] != $value) {
325 $document['$set'][$super_key] = $value;
329 foreach (array_diff(array_keys($past_values), array_keys($values)) as $key) {
330 $super_key = "{$parent_key}.{$key}";
331 $document['$unset'][$super_key] = 1;
334 return true;
336 // }}}
338 // array getCurrentDocument(bool $update) {{{
340 * Get Current Document
342 * Based on this object properties a new document (Array)
343 * is returned. If we're modifying an document, just the modified
344 * properties are included in this document, which uses $set,
345 * $unset, $pushAll and $pullAll.
348 * @param bool $update
350 * @return array
352 final protected function getCurrentDocument($update=false, $current=false)
354 $document = array();
355 $object = get_document_vars($this);
357 if (!$current) {
358 $current = (array)$this->_current;
362 $this->findReferences($object);
364 $this->triggerEvent('before_validate_'.($update?'update':'creation'), array(&$object));
365 $this->triggerEvent('before_validate', array(&$object));
367 foreach ($object as $key => $value) {
368 if (!$value) {
369 continue;
371 if ($update) {
372 if (is_array($value) && isset($current[$key])) {
374 * If the Field to update is an array, it has a different
375 * behaviour other than $set and $unset. Fist, we need
376 * need to check if it is an array or document, because
377 * they can't be mixed.
380 if (!is_array($current[$key])) {
382 * We're lucky, the field wasn't
383 * an array previously.
385 $this->runFilter($key, $value, $current[$key]);
386 $document['$set'][$key] = $value;
387 continue;
390 if (!$this->getCurrentSubDocument($document, $key, $value, $current[$key])) {
391 throw new Exception("{$key}: Array and documents are not compatible");
393 } else if(!isset($current[$key]) || $value !== $current[$key]) {
395 * It is 'linear' field that has changed, or
396 * has been modified.
398 $past_value = isset($current[$key]) ? $current[$key] : null;
399 $this->runFilter($key, $value, $past_value);
400 $document['$set'][$key] = $value;
402 } else {
404 * It is a document insertation, so we
405 * create the document.
407 $this->runFilter($key, $value, null);
408 $document[$key] = $value;
412 /* Updated behaves in a diff. way */
413 if ($update) {
414 foreach (array_diff(array_keys($this->_current), array_keys($object)) as $property) {
415 if ($property == '_id') {
416 continue;
418 $document['$unset'][$property] = 1;
422 if (count($document) == 0) {
423 return array();
426 $this->triggerEvent('after_validate_'.($update?'update':'creation'), array(&$object));
427 $this->triggerEvent('after_validate', array(&$document));
429 return $document;
431 // }}}
433 // }}}
435 // EVENT HANDLERS {{{
437 // addEvent($action, $callback) {{{
439 * addEvent
442 final static function addEvent($action, $callback)
444 if (!is_callable($callback)) {
445 throw new Exception("Invalid callback");
448 $class = get_called_class();
449 if ($class == __CLASS__) {
450 $events = & self::$_super_events;
451 } else {
452 $events = & self::$_events[$class];
454 if (!isset($events[$action])) {
455 $events[$action] = array();
457 $events[$action][] = $callback;
458 return true;
460 // }}}
462 // triggerEvent(string $event, Array $events_params) {{{
463 final function triggerEvent($event, Array $events_params = array())
465 $events = & self::$_events[get_class($this)][$event];
466 $sevents = & self::$_super_events[$event];
468 if (!is_array($events_params)) {
469 return false;
472 /* Super-Events handler receives the ActiveMongo class name as first param */
473 $sevents_params = array_merge(array(get_class($this)), $events_params);
475 foreach (array('events', 'sevents') as $event_type) {
476 if (count($$event_type) > 0) {
477 $params = "{$event_type}_params";
478 foreach ($$event_type as $fnc) {
479 call_user_func_array($fnc, $$params);
484 /* Some natives events are allowed to be called
485 * as methods, if they exists
487 switch ($event) {
488 case 'before_create':
489 case 'before_update':
490 case 'before_validate':
491 case 'before_delete':
492 case 'after_create':
493 case 'after_update':
494 case 'after_validate':
495 case 'after_delete':
496 $fnc = array($this, $event);
497 $params = "events_params";
498 if (is_callable($fnc)) {
499 call_user_func_array($fnc, $$params);
501 break;
504 // }}}
506 // void runFilter(string $key, mixed &$value, mixed $past_value) {{{
508 * *Internal Method*
510 * This method check if the current document property has
511 * a filter method, if so, call it.
513 * If the filter returns false, throw an Exception.
515 * @return void
517 protected function runFilter($key, &$value, $past_value)
519 $filter = array($this, "{$key}_filter");
520 if (is_callable($filter)) {
521 $filter = call_user_func_array($filter, array(&$value, $past_value));
522 if ($filter===false) {
523 throw new ActiveMongo_FilterException("{$key} filter failed");
525 $this->$key = $value;
528 // }}}
530 // }}}
532 // void setCursor(MongoCursor $obj) {{{
534 * Set Cursor
536 * This method receive a MongoCursor and make
537 * it iterable.
539 * @param MongoCursor $obj
541 * @return void
543 final protected function setCursor(MongoCursor $obj)
545 $this->_cursor = $obj;
546 $this->setResult($obj->getNext());
548 // }}}
550 // void setResult(Array $obj) {{{
552 * Set Result
554 * This method takes an document and copy it
555 * as properties in this object.
557 * @param Array $obj
559 * @return void
561 final protected function setResult($obj)
563 /* Unsetting previous results, if any */
564 foreach (array_keys(get_document_vars($this, false)) as $key) {
565 unset($this->$key);
567 $this->_id = null;
569 /* Add our current resultset as our object's property */
570 foreach ((array)$obj as $key => $value) {
571 if ($key[0] == '$') {
572 continue;
574 $this->$key = $value;
577 /* Save our record */
578 $this->_current = $obj;
580 // }}}
582 // this find([$_id]) {{{
584 * Simple find.
586 * Really simple find, which uses this object properties
587 * for fast filtering
589 * @return object this
591 final function find($_id = null)
593 $vars = get_document_vars($this);
594 foreach ($vars as $key => $value) {
595 if (!$value) {
596 unset($vars[$key]);
598 $parent_class = __CLASS__;
599 if ($value InstanceOf $parent_class) {
600 $this->getColumnDeference($vars, $key, $value);
601 unset($vars[$key]); /* delete old value */
604 if ($_id != null) {
605 if (is_array($_id)) {
606 $vars['_id'] = array('$in' => $_id);
607 } else {
608 $vars['_id'] = $_id;
611 $res = $this->_getCollection()->find($vars);
612 $this->setCursor($res);
613 return $this;
615 // }}}
617 // void save(bool $async) {{{
619 * Save
621 * This method save the current document in MongoDB. If
622 * we're modifying a document, a update is performed, otherwise
623 * the document is inserted.
625 * On updates, special operations such as $set, $pushAll, $pullAll
626 * and $unset in order to perform efficient updates
628 * @param bool $async
630 * @return void
632 final function save($async=true)
634 $update = isset($this->_id) && $this->_id InstanceOf MongoID;
635 $conn = $this->_getCollection();
636 $document = $this->getCurrentDocument($update);
637 $object = get_document_vars($this);
638 if (count($document) == 0) {
639 return; /*nothing to do */
642 /* PRE-save hook */
643 $this->triggerEvent('before_'.($update ? 'update' : 'create'), array(&$document, $object));
645 if ($update) {
646 $conn->update(array('_id' => $this->_id), $document, array('safe' => $async));
647 if (isset($document['$set'])) {
648 foreach ($document['$set'] as $key => $value) {
649 $this->_current[$key] = $value;
650 $this->$key = $value;
653 if (isset($document['$unset'])) {
654 foreach ($document['$unset'] as $key => $value) {
655 unset($this->_current[$key]);
656 unset($this->$key);
659 } else {
660 $conn->insert($document, $async);
661 $this->setResult($document);
664 $this->triggerEvent('after_'.($update ? 'update' : 'create'), array($document, $object));
666 // }}}
668 // bool delete() {{{
670 * Delete the current document
672 * @return bool
674 final function delete()
676 if ($this->_cursor InstanceOf MongoCursor) {
677 $document = array('_id' => $this->_id);
678 $this->triggerEvent('before_delete', array($document));
679 $result = $this->_getCollection()->remove($document);
680 $this->triggerEvent('after_delete', array($document));
681 $this->setResult(array());
682 return $result;
683 } else {
684 $criteria = (array) $this->_query['query'];
686 /* remove */
687 $this->_getCollection()->remove($criteria);
689 /* reset object */
690 $this->reset();
692 return true;
694 return false;
696 // }}}
698 // Update {{{
700 * Multiple updates.
702 * This method perform multiple updates when a given
703 * criteria matchs (using where).
705 * By default the update is perform safely, but it can be
706 * changed.
708 * After the operation is done, the criteria is deleted.
710 * @param array $value Values to set
711 * @param bool $safe Whether or not peform the operation safely
713 * @return bool
716 function update(Array $value, $safe=true)
718 $this->_assertNotInQuery();
720 $criteria = (array) $this->_query['query'];
721 $options = array('multiple' => true, 'safe' => $safe);
723 /* update */
724 $col = $this->_getCollection();
725 $col->update($criteria, array('$set' => $value), $options);
727 /* reset object */
728 $this->reset();
730 return true;
732 // }}}
734 // void drop() {{{
736 * Delete the current colleciton and all its documents
738 * @return void
740 final static function drop()
742 $class = get_called_class();
743 if ($class == __CLASS__) {
744 return false;
746 $obj = new $class;
747 return $obj->_getCollection()->drop();
749 // }}}
751 // int count() {{{
753 * Return the number of documents in the actual request. If
754 * we're not in a request, it will return 0.
756 * @return int
758 final function count()
760 if ($this->valid()) {
761 return $this->_cursor->count();
763 return 0;
765 // }}}
767 // void setup() {{{
769 * This method should contain all the indexes, and shard keys
770 * needed by the current collection. This try to make
771 * installation on development environments easier.
773 function setup()
776 // }}}
778 // bool addIndex(array $columns, array $options) {{{
780 * addIndex
782 * Create an Index in the current collection.
784 * @param array $columns L ist of columns
785 * @param array $options Options
787 * @return bool
789 final function addIndex($columns, $options=array())
791 $default_options = array(
792 'background' => 1,
795 foreach ($default_options as $option => $value) {
796 if (!isset($options[$option])) {
797 $options[$option] = $value;
801 $collection = $this->_getCollection();
803 return $collection->ensureIndex($columns, $options);
805 // }}}
807 // string __toString() {{{
809 * To String
811 * If this object is treated as a string,
812 * it would return its ID.
814 * @return string
816 final function __toString()
818 return (string)$this->getID();
820 // }}}
822 // array sendCmd(array $cmd) {{{
824 * This method sends a command to the current
825 * database.
827 * @param array $cmd Current command
829 * @return array
831 final protected function sendCmd($cmd)
833 return $this->_getConnection()->command($cmd);
835 // }}}
837 // ITERATOR {{{
839 // void reset() {{{
841 * Reset our Object, delete the current cursor if any, and reset
842 * unsets the values.
844 * @return void
846 final function reset()
848 $this->_properties = null;
849 $this->_cursor = null;
850 $this->_query = null;
851 $this->_sort = null;
852 $this->_limit = 0;
853 $this->_skip = 0;
854 $this->setResult(array());
856 // }}}
858 // bool valid() {{{
860 * Valid
862 * Return if we're on an iteration and if it is still valid
864 * @return true
866 final function valid()
868 if (!$this->_cursor InstanceOf MongoCursor) {
869 $this->doQuery();
871 return $this->_cursor InstanceOf MongoCursor && $this->_cursor->valid();
873 // }}}
875 // bool next() {{{
877 * Move to the next document
879 * @return bool
881 final function next()
883 if ($this->_cloned) {
884 throw new MongoException("Cloned objects can't iterate");
886 return $this->_cursor->next();
888 // }}}
890 // this current() {{{
892 * Return the current object, and load the current document
893 * as this object property
895 * @return object
897 final function current()
899 $this->setResult($this->_cursor->current());
900 return $this;
902 // }}}
904 // bool rewind() {{{
906 * Go to the first document
908 final function rewind()
910 if (!$this->_cursor InstanceOf MongoCursor) {
911 $this->doQuery();
913 return $this->_cursor->rewind();
915 // }}}
917 // }}}
919 // ARRAY ACCESS {{{
920 final function offsetExists($offset)
922 return isset($this->$offset);
925 final function offsetGet($offset)
927 return $this->$offset;
930 final function offsetSet($offset, $value)
932 $this->$offset = $value;
935 final function offsetUnset($offset)
937 unset($this->$offset);
939 // }}}
941 // REFERENCES {{{
943 // array getReference() {{{
945 * ActiveMongo extended the Mongo references, adding
946 * the concept of 'dynamic' requests, saving in the database
947 * the current query with its options (sort, limit, etc).
949 * This is useful to associate a document with a given
950 * request. To undestand this better please see the 'reference'
951 * example.
953 * @return array
955 final function getReference($dynamic=false)
957 if (!$this->getID() && !$dynamic) {
958 return null;
961 $document = array(
962 '$ref' => $this->getCollectionName(),
963 '$id' => $this->getID(),
964 '$db' => $this->getDatabaseName(),
965 'class' => get_class($this),
968 if ($dynamic && $this->_cursor InstanceOf MongoCursor) {
969 $cursor = $this->_cursor;
970 if (!is_callable(array($cursor, "Info"))) {
971 throw new Exception("Please upgrade your PECL/Mongo module to use this feature");
973 $document['dynamic'] = array();
974 $query = $cursor->Info();
975 foreach ($query as $type => $value) {
976 $document['dynamic'][$type] = $value;
979 return $document;
981 // }}}
983 // void getDocumentReferences($document, &$refs) {{{
985 * Get Current References
987 * Inspect the current document trying to get any references,
988 * if any.
990 * @param array $document Current document
991 * @param array &$refs References found in the document.
992 * @param array $parent_key Parent key
994 * @return void
996 final protected function getDocumentReferences($document, &$refs, $parent_key=null)
998 foreach ($document as $key => $value) {
999 if (is_array($value)) {
1000 if (MongoDBRef::isRef($value)) {
1001 $pkey = $parent_key;
1002 $pkey[] = $key;
1003 $refs[] = array('ref' => $value, 'key' => $pkey);
1004 } else {
1005 $parent_key[] = $key;
1006 $this->getDocumentReferences($value, $refs, $parent_key);
1011 // }}}
1013 // object _deferencingCreateObject(string $class) {{{
1015 * Called at deferencig time
1017 * Check if the given string is a class, and it is a sub class
1018 * of ActiveMongo, if it is instance and return the object.
1020 * @param string $class
1022 * @return object
1024 private function _deferencingCreateObject($class)
1026 if (!is_subclass_of($class, __CLASS__)) {
1027 throw new MongoException("Fatal Error, imposible to create ActiveMongo object of {$class}");
1029 return new $class;
1031 // }}}
1033 // void _deferencingRestoreProperty(array &$document, array $keys, mixed $req) {{{
1035 * Called at deferencig time
1037 * This method iterates $document until it could match $keys path, and
1038 * replace its value by $req.
1040 * @param array &$document Document to replace
1041 * @param array $keys Path of property to change
1042 * @param mixed $req Value to replace.
1044 * @return void
1046 private function _deferencingRestoreProperty(&$document, $keys, $req)
1048 $obj = & $document;
1050 /* find the $req proper spot */
1051 foreach ($keys as $key) {
1052 $obj = & $obj[$key];
1055 $obj = $req;
1057 /* Delete reference variable */
1058 unset($obj);
1060 // }}}
1062 // object _deferencingQuery($request) {{{
1064 * Called at deferencig time
1066 * This method takes a dynamic reference and request
1067 * it to MongoDB.
1069 * @param array $request Dynamic reference
1071 * @return this
1073 private function _deferencingQuery($request)
1075 $collection = $this->_getCollection();
1076 $cursor = $collection->find($request['query'], $request['fields']);
1077 if ($request['limit'] > 0) {
1078 $cursor->limit($request['limit']);
1080 if ($request['skip'] > 0) {
1081 $cursor->limit($request['limit']);
1084 $this->setCursor($cursor);
1086 return $this;
1088 // }}}
1090 // void doDeferencing() {{{
1092 * Perform a deferencing in the current document, if there is
1093 * any reference.
1095 * ActiveMongo will do its best to group references queries as much
1096 * as possible, in order to perform as less request as possible.
1098 * ActiveMongo doesn't rely on MongoDB references, but it can support
1099 * it, but it is prefered to use our referencing.
1101 * @experimental
1103 final function doDeferencing($refs=array())
1105 /* Get current document */
1106 $document = get_document_vars($this);
1108 if (count($refs)==0) {
1109 /* Inspect the whole document */
1110 $this->getDocumentReferences($document, $refs);
1113 $db = $this->_getConnection();
1115 /* Gather information about ActiveMongo Objects
1116 * that we need to create
1118 $classes = array();
1119 foreach ($refs as $ref) {
1120 if (!isset($ref['ref']['class'])) {
1122 /* Support MongoDBRef, we do our best to be compatible {{{ */
1123 /* MongoDB 'normal' reference */
1125 $obj = MongoDBRef::get($db, $ref['ref']);
1127 /* Offset the current document to the right spot */
1128 /* Very inefficient, never use it, instead use ActiveMongo References */
1130 $this->_deferencingRestoreProperty($document, $ref['key'], clone $req);
1132 /* Dirty hack, override our current document
1133 * property with the value itself, in order to
1134 * avoid replace a MongoDB reference by its content
1136 $this->_deferencingRestoreProperty($this->_current, $ref['key'], clone $req);
1138 /* }}} */
1140 } else {
1142 if (isset($ref['ref']['dynamic'])) {
1143 /* ActiveMongo Dynamic Reference */
1145 /* Create ActiveMongo object */
1146 $req = $this->_deferencingCreateObject($ref['ref']['class']);
1148 /* Restore saved query */
1149 $req->_deferencingQuery($ref['ref']['dynamic']);
1151 $results = array();
1153 /* Add the result set */
1154 foreach ($req as $result) {
1155 $results[] = clone $result;
1158 /* add information about the current reference */
1159 foreach ($ref['ref'] as $key => $value) {
1160 $results[$key] = $value;
1163 $this->_deferencingRestoreProperty($document, $ref['key'], $results);
1165 } else {
1166 /* ActiveMongo Reference FTW! */
1167 $classes[$ref['ref']['class']][] = $ref;
1172 /* {{{ Create needed objects to query MongoDB and replace
1173 * our references by its objects documents.
1175 foreach ($classes as $class => $refs) {
1176 $req = $this->_deferencingCreateObject($class);
1178 /* Load list of IDs */
1179 $ids = array();
1180 foreach ($refs as $ref) {
1181 $ids[] = $ref['ref']['$id'];
1184 /* Search to MongoDB once for all IDs found */
1185 $req->find($ids);
1187 if ($req->count() != count($refs)) {
1188 $total = $req->count();
1189 $expected = count($refs);
1190 throw new MongoException("Dereferencing error, MongoDB replied {$total} objects, we expected {$expected}");
1193 /* Replace our references by its objects */
1194 foreach ($refs as $ref) {
1195 $id = $ref['ref']['$id'];
1196 $place = $ref['key'];
1197 $req->rewind();
1198 while ($req->getID() != $id && $req->next());
1200 assert($req->getID() == $id);
1202 $this->_deferencingRestoreProperty($document, $place, clone $req);
1204 unset($obj);
1207 /* Release request, remember we
1208 * safely cloned it,
1210 unset($req);
1212 // }}}
1214 /* Replace the current document by the new deferenced objects */
1215 foreach ($document as $key => $value) {
1216 $this->$key = $value;
1219 // }}}
1221 // void getColumnDeference(&$document, $propety, ActiveMongo Obj) {{{
1223 * Prepare a "selector" document to search treaing the property
1224 * as a reference to the given ActiveMongo object.
1227 final function getColumnDeference(&$document, $property, ActiveMongo $obj)
1229 $document["{$property}.\$id"] = $obj->getID();
1231 // }}}
1233 // void findReferences(&$document) {{{
1235 * Check if in the current document to insert or update
1236 * exists any references to other ActiveMongo Objects.
1238 * @return void
1240 final function findReferences(&$document)
1242 if (!is_array($document)) {
1243 return;
1245 foreach($document as &$value) {
1246 $parent_class = __CLASS__;
1247 if (is_array($value)) {
1248 if (MongoDBRef::isRef($value)) {
1249 /* If the property we're inspecting is a reference,
1250 * we need to remove the values, restoring the valid
1251 * Reference.
1253 $arr = array(
1254 '$ref'=>1, '$id'=>1, '$db'=>1, 'class'=>1, 'dynamic'=>1
1256 foreach (array_keys($value) as $key) {
1257 if (!isset($arr[$key])) {
1258 unset($value[$key]);
1261 } else {
1262 $this->findReferences($value);
1264 } else if ($value InstanceOf $parent_class) {
1265 $value = $value->getReference();
1268 /* trick: delete last var. reference */
1269 unset($value);
1271 // }}}
1273 // void __clone() {{{
1274 /**
1275 * Cloned objects are rarely used, but ActiveMongo
1276 * uses it to create different objects per everyrecord,
1277 * which is used at deferencing. Therefore cloned object
1278 * do not contains the recordset, just the actual document,
1279 * so iterations are not allowed.
1282 final function __clone()
1284 unset($this->_cursor);
1285 $this->_cloned = true;
1287 // }}}
1289 // }}}
1291 // GET DOCUMENT ID {{{
1293 // getID() {{{
1295 * Return the current document ID. If there is
1296 * no document it would return false.
1298 * @return object|false
1300 final public function getID()
1302 if ($this->_id instanceof MongoID) {
1303 return $this->_id;
1305 return false;
1307 // }}}
1309 // string key() {{{
1311 * Return the current key
1313 * @return string
1315 final function key()
1317 return $this->getID();
1319 // }}}
1321 // }}}
1323 // Fancy (and silly) query abstraction {{{
1325 // _assertNotInQuery() {{{
1327 * Check if we can modify the query or not. We cannot modify
1328 * the query if we already asked to MongoDB, in this case the
1329 * object must be reset.
1331 * @return void
1333 final private function _assertNotInQuery()
1335 if ($this->_cursor InstanceOf MongoCursor) {
1336 throw new ActiveMongo_Exception("You cannot modify the query, please reset the object");
1339 // }}}
1341 // doQuery() {{{
1343 * Build the current request and send it to MongoDB.
1345 * @return this
1347 final function doQuery()
1349 $this->_assertNotInQuery();
1351 $col = $this->_getCollection();
1352 if (count($this->_properties) > 0) {
1353 $cursor = $col->find((array)$this->_query['query'], $this->_properties);
1354 } else {
1355 $cursor = $col->find((array)$this->_query['query']);
1357 if (is_array($this->_sort)) {
1358 $cursor->sort($this->_sort);
1360 if ($this->_limit > 0) {
1361 $cursor->limit($this->_limit);
1363 if ($this->_skip > 0) {
1364 $cursor->skip($this->_skip);
1367 /* Our cursor must be sent to ActiveMongo */
1368 $this->setCursor($cursor);
1370 return $this;
1372 // }}}
1374 // properties($props) {{{
1376 * Select 'properties' or 'columns' to be included in the document,
1377 * by default all properties are included.
1379 * @param array $props
1381 * @return this
1383 final function properties($props)
1385 $this->_assertNotInQuery();
1387 if (!is_array($props) && !is_string($props)) {
1388 return false;
1391 if (is_string($props)) {
1392 $props = explode(",", $props);
1395 foreach ($props as $id => $name) {
1396 $props[trim($name)] = 1;
1397 unset($props[$id]);
1400 $this->_properties = $props;
1402 return $this;
1405 final function columns($properties)
1407 return $this->properties($properties);
1409 // }}}
1411 // where($property, $value) {{{
1413 * Where abstraction.
1416 final function where($property_str, $value=null)
1418 $this->_assertNotInQuery();
1420 if (is_array($property_str)) {
1421 if ($value != null) {
1422 throw new ActiveMongo_Expception("Invalid parameters");
1424 foreach ($property_str as $property => $value) {
1425 if (is_numeric($property)) {
1426 $property = $value;
1427 $value = 0;
1429 $this->where($property, $value);
1431 return $this;
1434 $column = explode(" ", trim($property_str));
1435 if (count($column) != 1 && count($column) != 2) {
1436 throw new ActiveMongo_Exception("Failed while parsing '{$property_str}'");
1437 } else if (count($column) == 2) {
1439 $exp_scalar = true;
1440 switch (strtolower($column[1])) {
1441 case '>':
1442 case '$gt':
1443 $op = '$gt';
1444 break;
1446 case '>=':
1447 case '$gte':
1448 $op = '$gte';
1449 break;
1451 case '<':
1452 case '$lt':
1453 $op = '$lt';
1454 break;
1456 case '<=':
1457 case '$lte':
1458 $op = '$lte';
1459 break;
1461 case '==':
1462 case '$eq':
1463 case '=':
1464 if (is_array($value)) {
1465 $op = '$all';
1466 $exp_scalar = false;
1467 } else {
1468 $op = '$eq';
1470 break;
1472 case '!=':
1473 case '<>':
1474 case '$ne':
1475 if (is_array($value)) {
1476 $op = '$nin';
1477 $exp_scalar = false;
1478 } else {
1479 $op = '$ne';
1481 break;
1483 case '%':
1484 case 'mod':
1485 case '$mod':
1486 $op = '$mod';
1487 break;
1489 case 'exists':
1490 case '$exists':
1491 $value = true;
1492 $op = '$exists';
1493 break;
1495 /* regexp */
1496 case 'regexp':
1497 case 'regex':
1498 $value = new MongoRegex($value);
1499 $op = NULL;
1500 break;
1502 /* arrays */
1503 case 'in':
1504 case '$in':
1505 $exp_scalar = false;
1506 $op = '$in';
1507 break;
1509 case '$nin':
1510 case 'nin':
1511 $exp_scalar = false;
1512 $op = '$nin';
1513 break;
1516 /* geo operations */
1517 case 'near':
1518 case '$near':
1519 $op = '$near';
1520 $exp_scalar = false;
1521 break;
1523 default:
1524 throw new ActiveMongo_Exception("Failed to parse '{$column[1]}'");
1527 if ($exp_scalar && is_array($value)) {
1528 throw new ActiveMongo_Exception("Cannot use comparing operations with Array");
1529 } else if (!$exp_scalar && !is_array($value)) {
1530 throw new ActiveMongo_Exception("The operation {$column[1]} expected an Array");
1533 if ($op) {
1534 $value = array($op => $value);
1536 } else if (is_array($value)) {
1537 $value = array('$in' => $value);
1540 $spot = & $this->_query['query'][$column[0]];
1541 if (is_array($value)) {
1542 $spot[key($value)] = current($value);
1543 } else {
1544 /* simulate AND among same properties if
1545 * multiple values is passed for same property
1547 if (isset($spot)) {
1548 if (is_array($spot)) {
1549 $spot['$all'][] = $value;
1550 } else {
1551 $spot = array('$all' => array($spot, $value));
1553 } else {
1554 $spot = $value;
1558 return $this;
1560 // }}}
1562 // sort($sort_str) {{{
1564 * Abstract the documents sorting.
1566 * @param string $sort_str List of properties to use as sorting
1568 * @return this
1570 final function sort($sort_str)
1572 $this->_assertNotInQuery();
1574 $this->_sort = array();
1575 foreach ((array)explode(",", $sort_str) as $sort_part_str) {
1576 $sort_part = explode(" ", trim($sort_part_str), 2);
1577 switch(count($sort_part)) {
1578 case 1:
1579 $sort_part[1] = 'ASC';
1580 break;
1581 case 2:
1582 break;
1583 default:
1584 throw new ActiveMongo_Exception("Don't know how to parse {$sort_part_str}");
1587 switch (strtoupper($sort_part[1])) {
1588 case 'ASC':
1589 $sort_part[1] = 1;
1590 break;
1591 case 'DESC':
1592 $sort_part[1] = -1;
1593 break;
1594 default:
1595 throw new ActiveMongo_Exception("Invalid sorting direction `{$sort_part[1]}`");
1597 $this->_sort[ $sort_part[0] ] = $sort_part[1];
1600 return $this;
1602 // }}}
1604 // limit($limit, $skip) {{{
1606 * Abstract the limitation and pagination of documents.
1608 * @param int $limit Number of max. documents to retrieve
1609 * @param int $skip Number of documents to skip
1611 * @return this
1613 final function limit($limit=0, $skip=0)
1615 $this->_assertNotInQuery();
1617 if ($limit < 0 || $skip < 0) {
1618 return false;
1620 $this->_limit = $limit;
1621 $this->_skip = $skip;
1623 return $this;
1625 // }}}
1627 // }}}
1631 require_once dirname(__FILE__)."/Validators.php";
1632 require_once dirname(__FILE__)."/Exceptions.php";
1635 * Local variables:
1636 * tab-width: 4
1637 * c-basic-offset: 4
1638 * End:
1639 * vim600: sw=4 ts=4 fdm=marker
1640 * vim<600: sw=4 ts=4