Added tests (84,19% of coverance) to release first stable ASAP
[activemongo.git] / lib / ActiveMongo.php
blob2f5089c83fe706018a9057ed1f59440fffb20890
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 BSD License
66 * @package ActiveMongo
67 * @version 1.0
70 abstract class ActiveMongo implements Iterator, Countable, ArrayAccess
73 //{{{ Constants
74 const FIND_AND_MODIFY = 0x001;
75 // }}}
77 // properties {{{
78 /**
79 * Current databases objects
81 * @type array
83 private static $_dbs;
84 /**
85 * Current collections objects
87 * @type array
89 private static $_collections;
90 /**
91 * Current connection to MongoDB
93 * @type MongoConnection
95 private static $_conn;
96 /**
97 * Database name
99 * @type string
101 private static $_db;
103 * List of events handlers
105 * @type array
107 static private $_events = array();
109 * List of global events handlers
111 * @type array
113 static private $_super_events = array();
115 * Host name
117 * @type string
119 private static $_host;
121 * User (Auth)
123 * @type string
125 private static $_user;
128 * Password (Auth)
130 * @type string
132 private static $_pwd;
135 * Current document
137 * @type array
139 private $_current = array();
141 * Result cursor
143 * @type MongoCursor
145 private $_cursor = null;
147 * Extended result cursor, used for FindAndModify now
149 * @type int
151 private $_cursor_ex = null;
152 private $_cursor_ex_value;
154 * Count the findandmodify result counts
156 * @tyep array
158 private $_findandmodify_cnt = 0;
159 /* value to modify */
160 private $_findandmodify;
162 /* {{{ Silly but useful query abstraction */
163 private $_query = null;
164 private $_sort = null;
165 private $_limit = 0;
166 private $_skip = 0;
167 private $_properties = null;
168 /* }}} */
171 * Current document ID
173 * @type MongoID
175 private $_id;
178 * Tell if the current object
179 * is cloned or not.
181 * @type bool
183 private $_cloned = FALSE;
184 // }}}
186 // GET CONNECTION CONFIG {{{
188 // string getCollectionName() {{{
190 * Get Collection Name, by default the class name,
191 * but you it can be override at the class itself to give
192 * a custom name.
194 * @return string Collection Name
196 protected function getCollectionName()
198 if (isset($this)) {
199 return strtolower(get_class($this));
200 } else {
201 return strtolower(get_called_class());
204 // }}}
206 // string getDatabaseName() {{{
208 * Get Database Name, by default it is used
209 * the db name set by ActiveMong::connect()
211 * @return string DB Name
213 protected function getDatabaseName()
215 if (is_null(self::$_db)) {
216 throw new ActiveMongo_Exception("There is no information about the default DB name");
218 return self::$_db;
220 // }}}
222 // void install() {{{
224 * Install.
226 * This static method iterate over the classes lists,
227 * and execute the setup() method on every ActiveMongo
228 * subclass. You should do this just once.
231 final public static function install()
233 $classes = array_reverse(get_declared_classes());
234 foreach ($classes as $class)
236 if ($class == __CLASS__) {
237 break;
239 if (is_subclass_of($class, __CLASS__)) {
240 $obj = new $class;
241 $obj->setup();
245 // }}}
247 // void connection($db, $host) {{{
249 * Connect
251 * This method setup parameters to connect to a MongoDB
252 * database. The connection is done when it is needed.
254 * @param string $db Database name
255 * @param string $host Host to connect
256 * @param string $user User (Auth)
257 * @param string $pwd Password (Auth)
259 * @return void
261 final public static function connect($db, $host='localhost', $user = null, $pwd=null)
263 self::$_host = $host;
264 self::$_db = $db;
265 self::$_user = $user;
266 self::$_pwd = $pwd;
268 // }}}
270 // MongoConnection _getConnection() {{{
272 * Get Connection
274 * Get a valid database connection
276 * @return MongoConnection
278 final protected function _getConnection()
280 if (is_null(self::$_conn)) {
281 if (is_null(self::$_host)) {
282 self::$_host = 'localhost';
284 self::$_conn = new Mongo(self::$_host);
286 if (isset($this)) {
287 $dbname = $this->getDatabaseName();
288 } else {
289 $dbname = self::getDatabaseName();
291 if (!isSet(self::$_dbs[$dbname])) {
292 self::$_dbs[$dbname] = self::$_conn->selectDB($dbname);
294 if ( !is_null(self::$_user ) && !is_null(self::$_pwd ) ) {
295 self::$_dbs[$dbname]->authenticate(self::$_user,self::$_pwd);
299 return self::$_dbs[$dbname];
301 // }}}
303 // MongoCollection _getCollection() {{{
305 * Get Collection
307 * Get a collection connection.
309 * @return MongoCollection
311 final protected function _getCollection()
313 if (isset($this)) {
314 $colName = $this->getCollectionName();
315 } else {
316 $colName = self::getCollectionName();
318 if (!isset(self::$_collections[$colName])) {
319 self::$_collections[$colName] = self::_getConnection()->selectCollection($colName);
321 return self::$_collections[$colName];
323 // }}}
325 // }}}
327 // GET DOCUMENT TO SAVE OR UPDATE {{{
329 // getDocumentVars() {{{
331 * getDocumentVars
336 final protected function getDocumentVars()
338 $variables = array();
339 foreach ((array)$this->__sleep() as $var) {
340 if (!isset($this->$var) && $this->$var !== NULL) {
341 continue;
343 $variables[$var] = $this->$var;
345 return $variables;
347 // }}}
349 // bool getCurrentSubDocument(array &$document, string $parent_key, array $values, array $past_values) {{{
351 * Generate Sub-document
353 * This method build the difference between the current sub-document,
354 * and the origin one. If there is no difference, it would do nothing,
355 * otherwise it would build a document containing the differences.
357 * @param array &$document Document target
358 * @param string $parent_key Parent key name
359 * @param array $values Current values
360 * @param array $past_values Original values
362 * @return FALSE
364 final function getCurrentSubDocument(&$document, $parent_key, Array $values, Array $past_values)
367 * The current property is a embedded-document,
368 * now we're looking for differences with the
369 * previous value (because we're on an update).
371 * It behaves exactly as getCurrentDocument,
372 * but this is simples (it doesn't support
373 * yet filters)
375 foreach ($values as $key => $value) {
376 $super_key = "{$parent_key}.{$key}";
377 if (is_array($value)) {
379 * Inner document detected
381 if (!isset($past_values[$key]) || !is_array($past_values[$key])) {
383 * We're lucky, it is a new sub-document,
384 * we simple add it
386 $document['$set'][$super_key] = $value;
387 } else {
389 * This is a document like this, we need
390 * to find out the differences to avoid
391 * network overhead.
393 if (!$this->getCurrentSubDocument($document, $super_key, $value, $past_values[$key])) {
394 return FALSE;
397 continue;
398 } else if (!isset($past_values[$key]) || $past_values[$key] != $value) {
399 $document['$set'][$super_key] = $value;
403 foreach (array_diff(array_keys($past_values), array_keys($values)) as $key) {
404 $super_key = "{$parent_key}.{$key}";
405 $document['$unset'][$super_key] = 1;
408 return TRUE;
410 // }}}
412 // array getCurrentDocument(bool $update) {{{
414 * Get Current Document
416 * Based on this object properties a new document (Array)
417 * is returned. If we're modifying an document, just the modified
418 * properties are included in this document, which uses $set,
419 * $unset, $pushAll and $pullAll.
422 * @param bool $update
424 * @return array
426 final protected function getCurrentDocument($update=FALSE, $current=FALSE)
428 $document = array();
429 $object = $this->getDocumentVars();
431 if (!$current) {
432 $current = (array)$this->_current;
436 $this->findReferences($object);
438 $this->triggerEvent('before_validate', array(&$object));
439 $this->triggerEvent('before_validate_'.($update?'update':'creation'), array(&$object));
441 foreach ($object as $key => $value) {
442 if ($update) {
443 if (is_array($value) && isset($current[$key])) {
445 * If the Field to update is an array, it has a different
446 * behaviour other than $set and $unset. Fist, we need
447 * need to check if it is an array or document, because
448 * they can't be mixed.
451 if (!is_array($current[$key])) {
453 * We're lucky, the field wasn't
454 * an array previously.
456 $this->runFilter($key, $value, $current[$key]);
457 $document['$set'][$key] = $value;
458 continue;
461 if (!$this->getCurrentSubDocument($document, $key, $value, $current[$key])) {
462 throw new Exception("{$key}: Array and documents are not compatible");
464 } else if(!isset($current[$key]) || $value !== $current[$key]) {
466 * It is 'linear' field that has changed, or
467 * has been modified.
469 $past_value = isset($current[$key]) ? $current[$key] : null;
470 $this->runFilter($key, $value, $past_value);
471 $document['$set'][$key] = $value;
473 } else {
475 * It is a document insertation, so we
476 * create the document.
478 $this->runFilter($key, $value, null);
479 $document[$key] = $value;
483 /* Updated behaves in a diff. way */
484 if ($update) {
485 foreach (array_diff(array_keys($this->_current), array_keys($object)) as $property) {
486 if ($property == '_id') {
487 continue;
489 $document['$unset'][$property] = 1;
493 if (count($document) == 0) {
494 return array();
497 $this->triggerEvent('after_validate', array(&$document));
498 $this->triggerEvent('after_validate_'.($update?'update':'creation'), array(&$object));
500 return $document;
502 // }}}
504 // }}}
506 // EVENT HANDLERS {{{
508 // addEvent($action, $callback) {{{
510 * addEvent
513 final static function addEvent($action, $callback)
515 if (!is_callable($callback)) {
516 throw new Exception("Invalid callback");
519 $class = get_called_class();
520 if ($class == __CLASS__) {
521 $events = & self::$_super_events;
522 } else {
523 $events = & self::$_events[$class];
525 if (!isset($events[$action])) {
526 $events[$action] = array();
528 $events[$action][] = $callback;
529 return TRUE;
531 // }}}
533 // triggerEvent(string $event, Array $events_params) {{{
534 final function triggerEvent($event, Array $events_params = array())
536 if (!isset($this)) {
537 $class = get_called_class();
538 } else {
539 $class = get_class($this);
541 $events = & self::$_events[$class][$event];
542 $sevents = & self::$_super_events[$event];
544 if (!is_array($events_params)) {
545 return FALSE;
548 /* Super-Events handler receives the ActiveMongo class name as first param */
549 $sevents_params = array_merge(array($class), $events_params);
551 foreach (array('events', 'sevents') as $event_type) {
552 if (count($$event_type) > 0) {
553 $params = "{$event_type}_params";
554 foreach ($$event_type as $fnc) {
555 call_user_func_array($fnc, $$params);
560 /* Some natives events are allowed to be called
561 * as methods, if they exists
563 if (!isset($this)) {
564 return;
566 switch ($event) {
567 case 'before_create':
568 case 'before_update':
569 case 'before_validate':
570 case 'before_delete':
571 case 'before_drop':
572 case 'after_create':
573 case 'after_update':
574 case 'after_validate':
575 case 'after_delete':
576 case 'after_drop':
577 $fnc = array($this, $event);
578 $params = "events_params";
579 if (is_callable($fnc)) {
580 call_user_func_array($fnc, $$params);
582 break;
585 // }}}
587 // void runFilter(string $key, mixed &$value, mixed $past_value) {{{
589 * *Internal Method*
591 * This method check if the current document property has
592 * a filter method, if so, call it.
594 * If the filter returns FALSE, throw an Exception.
596 * @return void
598 protected function runFilter($key, &$value, $past_value)
600 $filter = array($this, "{$key}_filter");
601 if (is_callable($filter)) {
602 $filter = call_user_func_array($filter, array(&$value, $past_value));
603 if ($filter===FALSE) {
604 throw new ActiveMongo_FilterException("{$key} filter failed");
606 $this->$key = $value;
609 // }}}
611 // }}}
613 // void setCursor(MongoCursor $obj) {{{
615 * Set Cursor
617 * This method receive a MongoCursor and make
618 * it iterable.
620 * @param MongoCursor $obj
622 * @return void
624 final protected function setCursor(MongoCursor $obj)
626 $this->_cursor = $obj;
627 $this->setResult($obj->getNext());
629 // }}}
631 // void setResult(Array $obj) {{{
633 * Set Result
635 * This method takes an document and copy it
636 * as properties in this object.
638 * @param Array $obj
640 * @return void
642 final protected function setResult($obj)
644 /* Unsetting previous results, if any */
645 foreach (array_keys(get_document_vars($this, FALSE)) as $key) {
646 unset($this->$key);
648 $this->_id = null;
650 /* Add our current resultset as our object's property */
651 foreach ((array)$obj as $key => $value) {
652 if ($key[0] == '$') {
653 continue;
655 $this->$key = $value;
658 /* Save our record */
659 $this->_current = $obj;
661 // }}}
663 // this find([$_id]) {{{
665 * Simple find.
667 * Really simple find, which uses this object properties
668 * for fast filtering
670 * @return object this
672 final function find($_id = null)
674 $vars = get_document_vars($this);
675 foreach ($vars as $key => $value) {
676 if (!$value) {
677 unset($vars[$key]);
679 $parent_class = __CLASS__;
680 if ($value InstanceOf $parent_class) {
681 $this->getColumnDeference($vars, $key, $value);
682 unset($vars[$key]); /* delete old value */
685 if ($_id != null) {
686 if (is_array($_id)) {
687 $vars['_id'] = array('$in' => $_id);
688 } else {
689 $vars['_id'] = $_id;
692 $res = $this->_getCollection()->find($vars);
693 $this->setCursor($res);
694 return $this;
696 // }}}
698 // void save(bool $async) {{{
700 * Save
702 * This method save the current document in MongoDB. If
703 * we're modifying a document, a update is performed, otherwise
704 * the document is inserted.
706 * On updates, special operations such as $set, $pushAll, $pullAll
707 * and $unset in order to perform efficient updates
709 * @param bool $async
711 * @return void
713 final function save($async=TRUE)
715 $update = isset($this->_id) && $this->_id InstanceOf MongoID;
716 $conn = $this->_getCollection();
717 $document = $this->getCurrentDocument($update);
718 $object = $this->getDocumentVars();
719 if (count($document) == 0) {
720 return; /*nothing to do */
723 /* PRE-save hook */
724 $this->triggerEvent('before_'.($update ? 'update' : 'create'), array(&$document, $object));
726 if ($update) {
727 $conn->update(array('_id' => $this->_id), $document, array('safe' => $async));
728 if (isset($document['$set'])) {
729 foreach ($document['$set'] as $key => $value) {
730 $this->_current[$key] = $value;
731 $this->$key = $value;
734 if (isset($document['$unset'])) {
735 foreach ($document['$unset'] as $key => $value) {
736 unset($this->_current[$key]);
737 unset($this->$key);
740 } else {
741 $conn->insert($document, $async);
742 $this->setResult($document);
745 $this->triggerEvent('after_'.($update ? 'update' : 'create'), array($document, $object));
747 // }}}
749 // bool delete() {{{
751 * Delete the current document
753 * @return bool
755 final function delete()
758 $document = array('_id' => $this->_id);
759 if ($this->_cursor InstanceOf MongoCursor) {
760 $this->triggerEvent('before_delete', array($document));
761 $result = $this->_getCollection()->remove($document);
762 $this->triggerEvent('after_delete', array($document));
763 $this->setResult(array());
764 return $result;
765 } else {
766 $criteria = (array) $this->_query['query'];
768 /* remove */
769 $this->triggerEvent('before_delete', array($document));
770 $this->_getCollection()->remove($criteria);
771 $this->triggerEvent('after_delete', array($document));
773 /* reset object */
774 $this->reset();
776 return TRUE;
778 return FALSE;
780 // }}}
782 // Update {{{
784 * Multiple updates.
786 * This method perform multiple updates when a given
787 * criteria matchs (using where).
789 * By default the update is perform safely, but it can be
790 * changed.
792 * After the operation is done, the criteria is deleted.
794 * @param array $value Values to set
795 * @param bool $safe Whether or not peform the operation safely
797 * @return bool
800 function update(Array $value, $safe=TRUE)
802 $this->_assertNotInQuery();
804 $criteria = (array) $this->_query['query'];
805 $options = array('multiple' => TRUE, 'safe' => $safe);
807 /* update */
808 $col = $this->_getCollection();
809 $col->update($criteria, array('$set' => $value), $options);
811 /* reset object */
812 $this->reset();
814 return TRUE;
816 // }}}
818 // void drop() {{{
820 * Delete the current colleciton and all its documents
822 * @return void
824 final static function drop()
826 $class = get_called_class();
827 if ($class == __CLASS__) {
828 return FALSE;
830 $obj = new $class;
831 $obj->triggerEvent('before_drop');
832 $result = $obj->_getCollection()->drop();
833 $obj->triggerEvent('after_drop');
834 if ($result['ok'] != 1) {
835 throw new ActiveMongo_Exception($result['errmsg']);
837 return TRUE;
840 // }}}
842 // int count() {{{
844 * Return the number of documents in the actual request. If
845 * we're not in a request, it will return 0.
847 * @return int
849 final function count()
851 if ($this->valid()) {
852 return $this->_cursor->count();
854 return 0;
856 // }}}
858 // void setup() {{{
860 * This method should contain all the indexes, and shard keys
861 * needed by the current collection. This try to make
862 * installation on development environments easier.
864 function setup()
867 // }}}
869 // batchInsert {{{
870 /**
871 * Perform a batchInsert of objects.
873 * @param array $documents Arrays of documents to insert
874 * @param bool $safe True if a safe will be performed, this means data validation, and wait for MongoDB OK reply
875 * @param bool $on_error_continue If an error happen while validating an object, if it should continue or not
877 * @return bool
879 final public static function batchInsert(Array $documents, $safe=TRUE, $on_error_continue=TRUE)
881 if (__CLASS__ == get_called_class()) {
882 throw new ActiveMongo_Exception("Invalid batchInsert usage");
885 if ($safe) {
886 foreach ($documents as $id => $doc) {
887 $valid = FALSE;
888 if (is_array($doc)) {
889 try {
890 self::triggerEvent('before_create', array(&$doc));
891 self::triggerEvent('before_validate', array(&$doc, $doc));
892 self::triggerEvent('before_validate_creation', array(&$doc, $doc));
893 $valid = TRUE;
894 } catch (Exception $e) {}
896 if (!$valid) {
897 if (!$on_error_continue) {
898 throw new ActiveMongo_FilterException("Document $id is invalid");
900 unset($documents[$id]);
905 return self::_getCollection()->batchInsert($documents, array("safe" => $safe));
907 // }}}
909 // bool addIndex(array $columns, array $options) {{{
911 * addIndex
913 * Create an Index in the current collection.
915 * @param array $columns L ist of columns
916 * @param array $options Options
918 * @return bool
920 final function addIndex($columns, $options=array())
922 $default_options = array(
923 'background' => 1,
926 foreach ($default_options as $option => $value) {
927 if (!isset($options[$option])) {
928 $options[$option] = $value;
932 $collection = $this->_getCollection();
934 return $collection->ensureIndex($columns, $options);
936 // }}}
938 // Array getIndexes() {{{
940 * Return an array with all indexes
942 * @return array
944 final static function getIndexes()
946 return self::_getCollection()->getIndexInfo();
948 // }}}
950 // string __toString() {{{
952 * To String
954 * If this object is treated as a string,
955 * it would return its ID.
957 * @return string
959 function __toString()
961 return (string)$this->getID();
963 // }}}
965 // array sendCmd(array $cmd) {{{
967 * This method sends a command to the current
968 * database.
970 * @param array $cmd Current command
972 * @return array
974 final protected function sendCmd($cmd)
976 return $this->_getConnection()->command($cmd);
978 // }}}
980 // ITERATOR {{{
982 // void reset() {{{
984 * Reset our Object, delete the current cursor if any, and reset
985 * unsets the values.
987 * @return void
989 final function reset()
991 $this->_properties = null;
992 $this->_cursor = null;
993 $this->_cursor_ex = null;
994 $this->_query = null;
995 $this->_sort = null;
996 $this->_limit = 0;
997 $this->_skip = 0;
998 $this->setResult(array());
1000 // }}}
1002 // bool valid() {{{
1004 * Valid
1006 * Return if we're on an iteration and if it is still valid
1008 * @return TRUE
1010 final function valid()
1012 $valid = FALSE;
1013 if (!$this->_cursor_ex) {
1014 if (!$this->_cursor InstanceOf MongoCursor) {
1015 $this->doQuery();
1017 $valid = $this->_cursor InstanceOf MongoCursor && $this->_cursor->valid();
1018 } else {
1019 switch ($this->_cursor_ex) {
1020 case self::FIND_AND_MODIFY:
1021 if ($this->_limit > $this->_findandmodify_cnt) {
1022 $this->_execFindAndModify();
1023 $valid = $this->_cursor_ex_value['ok'] == 1;
1025 break;
1026 default:
1027 throw new ActiveMongo_Exception("Invalid _cursor_ex value");
1031 return $valid;
1033 // }}}
1035 // bool next() {{{
1037 * Move to the next document
1039 * @return bool
1041 final function next()
1043 if ($this->_cloned) {
1044 throw new ActiveMongo_Exception("Cloned objects can't iterate");
1046 if (!$this->_cursor_ex) {
1047 $result = $this->_cursor->next();
1048 $this->current();
1049 return $result;
1050 } else {
1051 switch ($this->_cursor_ex) {
1052 case self::FIND_AND_MODIFY:
1053 $this->_cursor_ex_value = NULL;
1054 break;
1055 default:
1056 throw new ActiveMongo_Exception("Invalid _cursor_ex value");
1060 // }}}
1062 // this current() {{{
1064 * Return the current object, and load the current document
1065 * as this object property
1067 * @return object
1069 final function current()
1071 if (!$this->_cursor_ex) {
1072 $this->setResult($this->_cursor->current());
1073 } else {
1074 switch ($this->_cursor_ex) {
1075 case self::FIND_AND_MODIFY:
1076 if (count($this->_cursor_ex_value) == 0) {
1077 $this->_execFindAndModify();
1079 $this->setResult($this->_cursor_ex_value['value']);
1080 break;
1081 default:
1082 throw new ActiveMongo_Exception("Invalid _cursor_ex value");
1085 return $this;
1087 // }}}
1089 // bool rewind() {{{
1091 * Go to the first document
1093 final function rewind()
1095 if (!$this->_cursor_ex) {
1096 /* rely on MongoDB cursor */
1097 if (!$this->_cursor InstanceOf MongoCursor) {
1098 $this->doQuery();
1100 $result = $this->_cursor->rewind();
1101 $this->current();
1102 return $result;
1103 } else {
1104 switch ($this->_cursor_ex) {
1105 case self::FIND_AND_MODIFY:
1106 $this->_findandmodify_cnt = 0;
1107 break;
1108 default:
1109 throw new ActiveMongo_Exception("Invalid _cursor_ex value");
1113 // }}}
1115 // }}}
1117 // ARRAY ACCESS {{{
1118 final function offsetExists($offset)
1120 return isset($this->$offset);
1123 final function offsetGet($offset)
1125 return $this->$offset;
1128 final function offsetSet($offset, $value)
1130 $this->$offset = $value;
1133 final function offsetUnset($offset)
1135 unset($this->$offset);
1137 // }}}
1139 // REFERENCES {{{
1141 // array getReference() {{{
1143 * ActiveMongo extended the Mongo references, adding
1144 * the concept of 'dynamic' requests, saving in the database
1145 * the current query with its options (sort, limit, etc).
1147 * This is useful to associate a document with a given
1148 * request. To undestand this better please see the 'reference'
1149 * example.
1151 * @return array
1153 final function getReference($dynamic=FALSE)
1155 if (!$this->getID() && !$dynamic) {
1156 return NULL;
1159 $document = array(
1160 '$ref' => $this->getCollectionName(),
1161 '$id' => $this->getID(),
1162 '$db' => $this->getDatabaseName(),
1163 'class' => get_class($this),
1166 if ($dynamic && $this->_cursor InstanceOf MongoCursor) {
1167 $cursor = $this->_cursor;
1168 if (!is_callable(array($cursor, "Info"))) {
1169 throw new Exception("Please upgrade your PECL/Mongo module to use this feature");
1171 $document['dynamic'] = array();
1172 $query = $cursor->Info();
1173 foreach ($query as $type => $value) {
1174 $document['dynamic'][$type] = $value;
1177 return $document;
1179 // }}}
1181 // void getDocumentReferences($document, &$refs) {{{
1183 * Get Current References
1185 * Inspect the current document trying to get any references,
1186 * if any.
1188 * @param array $document Current document
1189 * @param array &$refs References found in the document.
1190 * @param array $parent_key Parent key
1192 * @return void
1194 final protected function getDocumentReferences($document, &$refs, $parent_key=null)
1196 foreach ($document as $key => $value) {
1197 if (is_array($value)) {
1198 if (MongoDBRef::isRef($value)) {
1199 $pkey = $parent_key;
1200 $pkey[] = $key;
1201 $refs[] = array('ref' => $value, 'key' => $pkey);
1202 } else {
1203 $parent_key1 = $parent_key;
1204 $parent_key1[] = $key;
1205 $this->getDocumentReferences($value, $refs, $parent_key1);
1210 // }}}
1212 // object _deferencingCreateObject(string $class) {{{
1214 * Called at deferencig time
1216 * Check if the given string is a class, and it is a sub class
1217 * of ActiveMongo, if it is instance and return the object.
1219 * @param string $class
1221 * @return object
1223 private function _deferencingCreateObject($class)
1225 if (!is_subclass_of($class, __CLASS__)) {
1226 throw new ActiveMongo_Exception("Fatal Error, imposible to create ActiveMongo object of {$class}");
1228 return new $class;
1230 // }}}
1232 // void _deferencingRestoreProperty(array &$document, array $keys, mixed $req) {{{
1234 * Called at deferencig time
1236 * This method iterates $document until it could match $keys path, and
1237 * replace its value by $req.
1239 * @param array &$document Document to replace
1240 * @param array $keys Path of property to change
1241 * @param mixed $req Value to replace.
1243 * @return void
1245 private function _deferencingRestoreProperty(&$document, $keys, $req)
1247 $obj = & $document;
1249 /* find the $req proper spot */
1250 foreach ($keys as $key) {
1251 $obj = & $obj[$key];
1254 $obj = $req;
1256 /* Delete reference variable */
1257 unset($obj);
1259 // }}}
1261 // object _deferencingQuery($request) {{{
1263 * Called at deferencig time
1265 * This method takes a dynamic reference and request
1266 * it to MongoDB.
1268 * @param array $request Dynamic reference
1270 * @return this
1272 private function _deferencingQuery($request)
1274 $collection = $this->_getCollection();
1275 $cursor = $collection->find($request['query'], $request['fields']);
1276 if ($request['limit'] > 0) {
1277 $cursor->limit($request['limit']);
1279 if ($request['skip'] > 0) {
1280 $cursor->limit($request['limit']);
1283 $this->setCursor($cursor);
1285 return $this;
1287 // }}}
1289 // void doDeferencing() {{{
1291 * Perform a deferencing in the current document, if there is
1292 * any reference.
1294 * ActiveMongo will do its best to group references queries as much
1295 * as possible, in order to perform as less request as possible.
1297 * ActiveMongo doesn't rely on MongoDB references, but it can support
1298 * it, but it is prefered to use our referencing.
1300 * @experimental
1302 final function doDeferencing($refs=array())
1304 /* Get current document */
1305 $document = get_document_vars($this);
1307 if (count($refs)==0) {
1308 /* Inspect the whole document */
1309 $this->getDocumentReferences($document, $refs);
1312 $db = $this->_getConnection();
1314 /* Gather information about ActiveMongo Objects
1315 * that we need to create
1317 $classes = array();
1318 foreach ($refs as $ref) {
1319 if (!isset($ref['ref']['class'])) {
1321 /* Support MongoDBRef, we do our best to be compatible {{{ */
1322 /* MongoDB 'normal' reference */
1324 $obj = MongoDBRef::get($db, $ref['ref']);
1326 /* Offset the current document to the right spot */
1327 /* Very inefficient, never use it, instead use ActiveMongo References */
1329 $this->_deferencingRestoreProperty($document, $ref['key'], clone $req);
1331 /* Dirty hack, override our current document
1332 * property with the value itself, in order to
1333 * avoid replace a MongoDB reference by its content
1335 $this->_deferencingRestoreProperty($this->_current, $ref['key'], clone $req);
1337 /* }}} */
1339 } else {
1341 if (isset($ref['ref']['dynamic'])) {
1342 /* ActiveMongo Dynamic Reference */
1344 /* Create ActiveMongo object */
1345 $req = $this->_deferencingCreateObject($ref['ref']['class']);
1347 /* Restore saved query */
1348 $req->_deferencingQuery($ref['ref']['dynamic']);
1350 $results = array();
1352 /* Add the result set */
1353 foreach ($req as $result) {
1354 $results[] = clone $result;
1357 /* add information about the current reference */
1358 foreach ($ref['ref'] as $key => $value) {
1359 $results[$key] = $value;
1362 $this->_deferencingRestoreProperty($document, $ref['key'], $results);
1364 } else {
1365 /* ActiveMongo Reference FTW! */
1366 $classes[$ref['ref']['class']][] = $ref;
1371 /* {{{ Create needed objects to query MongoDB and replace
1372 * our references by its objects documents.
1374 foreach ($classes as $class => $refs) {
1375 $req = $this->_deferencingCreateObject($class);
1377 /* Load list of IDs */
1378 $ids = array();
1379 foreach ($refs as $ref) {
1380 $ids[] = $ref['ref']['$id'];
1383 /* Search to MongoDB once for all IDs found */
1384 $req->find($ids);
1387 /* Replace our references by its objects */
1388 foreach ($refs as $ref) {
1389 $id = $ref['ref']['$id'];
1390 $place = $ref['key'];
1391 $req->rewind();
1392 while ($req->getID() != $id && $req->next());
1394 assert($req->getID() == $id);
1396 $this->_deferencingRestoreProperty($document, $place, clone $req);
1398 unset($obj);
1401 /* Release request, remember we
1402 * safely cloned it,
1404 unset($req);
1406 // }}}
1408 /* Replace the current document by the new deferenced objects */
1409 foreach ($document as $key => $value) {
1410 $this->$key = $value;
1413 // }}}
1415 // void getColumnDeference(&$document, $propety, ActiveMongo Obj) {{{
1417 * Prepare a "selector" document to search treaing the property
1418 * as a reference to the given ActiveMongo object.
1421 final function getColumnDeference(&$document, $property, ActiveMongo $obj)
1423 $document["{$property}.\$id"] = $obj->getID();
1425 // }}}
1427 // void findReferences(&$document) {{{
1429 * Check if in the current document to insert or update
1430 * exists any references to other ActiveMongo Objects.
1432 * @return void
1434 final function findReferences(&$document)
1436 if (!is_array($document)) {
1437 return;
1439 foreach($document as &$value) {
1440 $parent_class = __CLASS__;
1441 if (is_array($value)) {
1442 if (MongoDBRef::isRef($value)) {
1443 /* If the property we're inspecting is a reference,
1444 * we need to remove the values, restoring the valid
1445 * Reference.
1447 $arr = array(
1448 '$ref'=>1, '$id'=>1, '$db'=>1, 'class'=>1, 'dynamic'=>1
1450 foreach (array_keys($value) as $key) {
1451 if (!isset($arr[$key])) {
1452 unset($value[$key]);
1455 } else {
1456 $this->findReferences($value);
1458 } else if ($value InstanceOf $parent_class) {
1459 $value = $value->getReference();
1462 /* trick: delete last var. reference */
1463 unset($value);
1465 // }}}
1467 // void __clone() {{{
1468 /**
1469 * Cloned objects are rarely used, but ActiveMongo
1470 * uses it to create different objects per everyrecord,
1471 * which is used at deferencing. Therefore cloned object
1472 * do not contains the recordset, just the actual document,
1473 * so iterations are not allowed.
1476 final function __clone()
1478 unset($this->_cursor);
1479 $this->_cloned = TRUE;
1481 // }}}
1483 // }}}
1485 // GET DOCUMENT ID {{{
1487 // getID() {{{
1489 * Return the current document ID. If there is
1490 * no document it would return FALSE.
1492 * @return object|FALSE
1494 final public function getID()
1496 if ($this->_id instanceof MongoID) {
1497 return $this->_id;
1499 return FALSE;
1501 // }}}
1503 // string key() {{{
1505 * Return the current key
1507 * @return string
1509 final function key()
1511 return (string)$this->getID();
1513 // }}}
1515 // }}}
1517 // Fancy (and silly) query abstraction {{{
1519 // _assertNotInQuery() {{{
1521 * Check if we can modify the query or not. We cannot modify
1522 * the query if we're iterating over and oldest query, in this case the
1523 * object must be reset.
1525 * @return void
1527 final private function _assertNotInQuery()
1529 if ($this->_cursor InstanceOf MongoCursor || $this->_cursor_ex != NULL) {
1530 throw new ActiveMongo_Exception("You cannot modify the query, please reset the object");
1533 // }}}
1535 // doQuery() {{{
1537 * Build the current request and send it to MongoDB.
1539 * @return this
1541 final function doQuery()
1543 if ($this->_cursor_ex) {
1544 switch ($this->_cursor_ex) {
1545 case self::FIND_AND_MODIFY:
1546 $this->_cursor_ex_value = NULL;
1547 return;
1548 default:
1549 throw new ActiveMongo_Exception("Invalid _cursor_ex value");
1552 $this->_assertNotInQuery();
1554 $col = $this->_getCollection();
1555 if (count($this->_properties) > 0) {
1556 $cursor = $col->find((array)$this->_query['query'], $this->_properties);
1557 } else {
1558 $cursor = $col->find((array)$this->_query['query']);
1560 if (is_array($this->_sort)) {
1561 $cursor->sort($this->_sort);
1563 if ($this->_limit > 0) {
1564 $cursor->limit($this->_limit);
1566 if ($this->_skip > 0) {
1567 $cursor->skip($this->_skip);
1570 /* Our cursor must be sent to ActiveMongo */
1571 $this->setCursor($cursor);
1573 return $this;
1575 // }}}
1577 // properties($props) {{{
1579 * Select 'properties' or 'columns' to be included in the document,
1580 * by default all properties are included.
1582 * @param array $props
1584 * @return this
1586 final function properties($props)
1588 $this->_assertNotInQuery();
1590 if (!is_array($props) && !is_string($props)) {
1591 return FALSE;
1594 if (is_string($props)) {
1595 $props = explode(",", $props);
1598 foreach ($props as $id => $name) {
1599 $props[trim($name)] = 1;
1600 unset($props[$id]);
1603 $this->_properties = $props;
1605 return $this;
1608 final function columns($properties)
1610 return $this->properties($properties);
1612 // }}}
1614 // where($property, $value) {{{
1616 * Where abstraction.
1619 final function where($property_str, $value=null)
1621 $this->_assertNotInQuery();
1623 if (is_array($property_str)) {
1624 if ($value != null) {
1625 throw new ActiveMongo_Exception("Invalid parameters");
1627 foreach ($property_str as $property => $value) {
1628 if (is_numeric($property)) {
1629 $property = $value;
1630 $value = 0;
1632 $this->where($property, $value);
1634 return $this;
1637 $column = explode(" ", trim($property_str));
1638 if (count($column) != 1 && count($column) != 2) {
1639 throw new ActiveMongo_Exception("Failed while parsing '{$property_str}'");
1640 } else if (count($column) == 2) {
1642 $exp_scalar = TRUE;
1643 switch (strtolower($column[1])) {
1644 case '>':
1645 case '$gt':
1646 $op = '$gt';
1647 break;
1649 case '>=':
1650 case '$gte':
1651 $op = '$gte';
1652 break;
1654 case '<':
1655 case '$lt':
1656 $op = '$lt';
1657 break;
1659 case '<=':
1660 case '$lte':
1661 $op = '$lte';
1662 break;
1664 case '==':
1665 case '$eq':
1666 case '=':
1667 if (is_array($value)) {
1668 $op = '$all';
1669 $exp_scalar = FALSE;
1670 } else {
1671 $op = '$eq';
1673 break;
1675 case '!=':
1676 case '<>':
1677 case '$ne':
1678 if (is_array($value)) {
1679 $op = '$nin';
1680 $exp_scalar = FALSE;
1681 } else {
1682 $op = '$ne';
1684 break;
1686 case '%':
1687 case 'mod':
1688 case '$mod':
1689 $op = '$mod';
1690 break;
1692 case 'exists':
1693 case '$exists':
1694 if ($value === NULL) {
1695 $value = TRUE;
1697 $op = '$exists';
1698 break;
1700 /* regexp */
1701 case 'regexp':
1702 case 'regex':
1703 $value = new MongoRegex($value);
1704 $op = NULL;
1705 break;
1707 /* arrays */
1708 case 'in':
1709 case '$in':
1710 $exp_scalar = FALSE;
1711 $op = '$in';
1712 break;
1714 case '$nin':
1715 case 'nin':
1716 $exp_scalar = FALSE;
1717 $op = '$nin';
1718 break;
1721 /* geo operations */
1722 case 'near':
1723 case '$near':
1724 $op = '$near';
1725 $exp_scalar = FALSE;
1726 break;
1728 default:
1729 throw new ActiveMongo_Exception("Failed to parse '{$column[1]}'");
1732 if ($exp_scalar && is_array($value)) {
1733 throw new ActiveMongo_Exception("Cannot use comparing operations with Array");
1734 } else if (!$exp_scalar && !is_array($value)) {
1735 throw new ActiveMongo_Exception("The operation {$column[1]} expected an Array");
1738 if ($op) {
1739 $value = array($op => $value);
1741 } else if (is_array($value)) {
1742 $value = array('$in' => $value);
1745 $spot = & $this->_query['query'][$column[0]];
1746 if (is_array($value)) {
1747 $spot[key($value)] = current($value);
1748 } else {
1749 /* simulate AND among same properties if
1750 * multiple values is passed for same property
1752 if (isset($spot)) {
1753 if (is_array($spot)) {
1754 $spot['$all'][] = $value;
1755 } else {
1756 $spot = array('$all' => array($spot, $value));
1758 } else {
1759 $spot = $value;
1763 return $this;
1765 // }}}
1767 // sort($sort_str) {{{
1769 * Abstract the documents sorting.
1771 * @param string $sort_str List of properties to use as sorting
1773 * @return this
1775 final function sort($sort_str)
1777 $this->_assertNotInQuery();
1779 $this->_sort = array();
1780 foreach ((array)explode(",", $sort_str) as $sort_part_str) {
1781 $sort_part = explode(" ", trim($sort_part_str), 2);
1782 switch(count($sort_part)) {
1783 case 1:
1784 $sort_part[1] = 'ASC';
1785 break;
1786 case 2:
1787 break;
1788 default:
1789 throw new ActiveMongo_Exception("Don't know how to parse {$sort_part_str}");
1792 switch (strtoupper($sort_part[1])) {
1793 case 'ASC':
1794 $sort_part[1] = 1;
1795 break;
1796 case 'DESC':
1797 $sort_part[1] = -1;
1798 break;
1799 default:
1800 throw new ActiveMongo_Exception("Invalid sorting direction `{$sort_part[1]}`");
1802 $this->_sort[ $sort_part[0] ] = $sort_part[1];
1805 return $this;
1807 // }}}
1809 // limit($limit, $skip) {{{
1811 * Abstract the limitation and pagination of documents.
1813 * @param int $limit Number of max. documents to retrieve
1814 * @param int $skip Number of documents to skip
1816 * @return this
1818 final function limit($limit=0, $skip=0)
1820 $this->_assertNotInQuery();
1822 if ($limit < 0 || $skip < 0) {
1823 return FALSE;
1825 $this->_limit = $limit;
1826 $this->_skip = $skip;
1828 return $this;
1830 // }}}
1832 // FindAndModify(Array $document) {{{
1834 * findAndModify
1838 final function findAndModify($document)
1840 $this->_assertNotInQuery();
1842 if (count($document) === 0) {
1843 throw new ActiveMongo_Exception("Empty \$document is not allowed");
1846 $this->_cursor_ex = self::FIND_AND_MODIFY;
1847 $this->_findandmodify = $document;
1849 return $this;
1852 private function _execFindAndModify()
1854 $query = (array)$this->_query['query'];
1856 $query = array(
1857 "findandmodify" => $this->getCollectionName(),
1858 "query" => $query,
1859 "update" => array('$set' => $this->_findandmodify),
1860 "new" => TRUE,
1862 $this->_cursor_ex_value = $this->sendCMD($query);
1863 if (isset($this->_query['sort'])) {
1864 $query["sort"] = $this->_query['sort'];
1867 $this->_findandmodify_cnt++;
1869 // }}}
1871 // }}}
1873 // __sleep() {{{
1875 * Return a list of properties to serialize, to save
1876 * into MongoDB
1878 * @return array
1880 function __sleep()
1882 return array_keys(get_document_vars($this));
1884 // }}}
1888 require_once dirname(__FILE__)."/Validators.php";
1889 require_once dirname(__FILE__)."/Exceptions.php";
1892 * Local variables:
1893 * tab-width: 4
1894 * c-basic-offset: 4
1895 * End:
1896 * vim600: sw=4 ts=4 fdm=marker
1897 * vim<600: sw=4 ts=4