Bugfix for delete (wasn't working properly with extended cursors)
[activemongo.git] / lib / ActiveMongo.php
blobafe42f822b8b41810f2ae7acd632cb7a0590d99d
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 if (version_compare(PHP_VERSION, '5.3') < 0) {
58 require dirname(__FILE__)."/Objects_compat.php";
59 } else {
60 require dirname(__FILE__)."/Objects.php";
63 /**
64 * ActiveMongo
66 * Simple ActiveRecord pattern built on top of MongoDB. This class
67 * aims to provide easy iteration, data validation before update,
68 * and efficient update.
70 * @author César D. Rodas <crodas@php.net>
71 * @license BSD License
72 * @package ActiveMongo
73 * @version 1.0
76 abstract class ActiveMongo implements Iterator, Countable, ArrayAccess
79 //{{{ Constants
80 const FIND_AND_MODIFY = 0x001;
81 // }}}
83 // properties {{{
84 /**
85 * Current databases objects
87 * @type array
89 private static $_dbs;
90 /**
91 * Current namespace
93 * @type string
95 private static $_namespace = NULL;
96 /**
97 * Specific namespaces for each class
99 * @type string
101 private static $_namespaces = array();
103 * Current collections objects
105 * @type array
107 private static $_collections;
109 * Current connection to MongoDB
111 * @type MongoConnection
113 private static $_conn;
115 * Database name
117 * @type string
119 private static $_db;
121 * List of events handlers
123 * @type array
125 static private $_events = array();
127 * List of global events handlers
129 * @type array
131 static private $_super_events = array();
133 * Host name
135 * @type string
137 private static $_host;
139 * User (Auth)
141 * @type string
143 private static $_user;
146 * Password (Auth)
148 * @type string
150 private static $_pwd;
153 * Current document
155 * @type array
157 private $_current = array();
159 * Result cursor
161 * @type MongoCursor
163 private $_cursor = NULL;
165 * Extended result cursor, used for FindAndModify now
167 * @type int
169 private $_cursor_ex = NULL;
170 private $_cursor_ex_value;
172 * Count the findandmodify result counts
174 * @tyep array
176 private $_findandmodify_cnt = 0;
177 /* value to modify */
178 private $_findandmodify;
180 /* {{{ Silly but useful query abstraction */
181 private $_cached = FALSE;
182 private $_query = NULL;
183 private $_sort = NULL;
184 private $_limit = 0;
185 private $_skip = 0;
186 private $_properties = NULL;
187 /* }}} */
190 * Current document ID
192 * @type MongoID
194 private $_id;
197 * Tell if the current object
198 * is cloned or not.
200 * @type bool
202 private $_cloned = FALSE;
203 // }}}
205 final static function isAbstractChildClass($class)
207 $r = new ReflectionClass($class);
208 if ($r->IsAbstract())
210 // make sure it's a child
211 if ($r->isSubclassOf(__CLASS__))
213 return true;
216 return false;
219 // GET CONNECTION CONFIG {{{
221 // setNameSpace($namespace='') {{{
223 * Set a namespace for all connections is it is called
224 * statically from ActiveMongo or for specific classes
225 * if it is called from an instance.
227 * @param string $namespace
229 * @return bool
231 final static function setNamespace($namespace='')
233 if (preg_match("#^[\-\_a-z0-9]*$#i", $namespace)) {
234 /* sort of standard late binding */
235 if (isset($this)) {
236 $context = get_class($this);
237 } else {
238 $context = get_called_class();
241 if ($context == __CLASS__ || self::isAbstractChildClass($context)) {
242 self::$_namespace = $namespace;
243 } else {
244 self::$_namespaces[$context] = $namespace;
246 return TRUE;
248 return FALSE;
250 // }}}
252 // collectionName() {{{
254 * Get CollectionName
256 * Return the collection name (along with its namespace) for
257 * the current object.
259 * Warning: This must not be called statically from outside the
260 * fundtion.
262 * @return string
264 final public function collectionName()
266 $parent = __CLASS__;
267 /* Need to check if $this is instance of $parent
268 * because PHP5.2 fails detecting $this when a non-static
269 * method is called statically from another class ($this is
270 * inherited)
272 if (isset($this) && $this InstanceOf $parent) {
273 $collection = $this->getCollectionName();
274 $context = get_class($this);
275 } else {
276 /* ugly, it might fail if getCollectionName has some refernce to $this */
277 $context = get_called_class();
278 $collection = call_user_func(array($context, 'getCollectionName'));
281 if (isset(self::$_namespaces[$context]) && self::$_namespaces[$context]) {
282 $collection = self::$_namespaces[$context].".{$collection}";
283 } else if (self::$_namespace) {
284 $collection = self::$_namespace.".{$collection}";
287 return $collection;
289 // }}}
291 // string getCollectionName() {{{
293 * Get Collection Name, by default the class name,
294 * but you it can be override at the class itself to give
295 * a custom name.
297 * @return string Collection Name
299 protected function getCollectionName()
301 if (isset($this)) {
302 return strtolower(get_class($this));
303 } else {
304 return strtolower(get_called_class());
307 // }}}
309 // string getDatabaseName() {{{
311 * Get Database Name, by default it is used
312 * the db name set by ActiveMong::connect()
314 * @return string DB Name
316 protected function getDatabaseName()
318 if (is_NULL(self::$_db)) {
319 throw new ActiveMongo_Exception("There is no information about the default DB name");
321 return self::$_db;
323 // }}}
325 // void install() {{{
327 * Install.
329 * This static method iterate over the classes lists,
330 * and execute the setup() method on every ActiveMongo
331 * subclass. You should do this just once.
334 final public static function install()
336 $classes = array_reverse(get_declared_classes());
337 foreach ($classes as $class)
339 $r = new ReflectionClass($class);
340 if ($r->IsAbstract()) // skip abstract ones
342 continue;
345 if ($class == __CLASS__) {
346 break;
348 if (is_subclass_of($class, __CLASS__)) {
349 $obj = new $class;
350 $obj->setup();
354 // }}}
356 // void connection($db, $host) {{{
358 * Connect
360 * This method setup parameters to connect to a MongoDB
361 * database. The connection is done when it is needed.
363 * @param string $db Database name
364 * @param string $host Host to connect
365 * @param string $user User (Auth)
366 * @param string $pwd Password (Auth)
368 * @return void
370 final public static function connect($db, $host='localhost', $user = NULL, $pwd=NULL)
372 self::$_host = $host;
373 self::$_db = $db;
374 self::$_user = $user;
375 self::$_pwd = $pwd;
377 // }}}
379 // isConnected() {{{
381 * Return TRUE is there is any active connection
382 * to MongoDB.
384 * @return bool
386 final public static function isConnected()
388 return !is_null(self::$_conn) || count(self::$_dbs) > 0 || count(self::$_collections) > 0;
390 // }}}
392 // disconnect() {{{
394 * Destroy all connections objects to MongoDB if any.
396 * @return void
398 final public static function disconnect()
400 self::$_conn = NULL;
401 self::$_dbs = array();
402 self::$_collections = array();
404 // }}}
406 // MongoConnection _getConnection() {{{
408 * Get Connection
410 * Get a valid database connection
412 * @return MongoConnection
414 final protected function _getConnection()
416 if (is_NULL(self::$_conn)) {
417 if (is_NULL(self::$_host)) {
418 self::$_host = 'localhost';
420 self::$_conn = new Mongo(self::$_host);
422 if (isset($this)) {
423 $dbname = $this->getDatabaseName();
424 } else {
425 $dbname = self::getDatabaseName();
427 if (!isSet(self::$_dbs[$dbname])) {
428 self::$_dbs[$dbname] = self::$_conn->selectDB($dbname);
430 if ( !is_NULL(self::$_user ) && !is_NULL(self::$_pwd ) ) {
431 self::$_dbs[$dbname]->authenticate(self::$_user,self::$_pwd);
435 return self::$_dbs[$dbname];
437 // }}}
439 // MongoCollection _getCollection() {{{
441 * Get Collection
443 * Get a collection connection.
445 * @return MongoCollection
447 final protected function _getCollection()
449 if (isset($this)) {
450 $colName = $this->CollectionName();
451 } else {
452 $colName = self::CollectionName();
454 if (!isset(self::$_collections[$colName])) {
455 self::$_collections[$colName] = self::_getConnection()->selectCollection($colName);
457 return self::$_collections[$colName];
459 // }}}
461 // }}}
463 // GET DOCUMENT TO SAVE OR UPDATE {{{
465 // getDocumentVars() {{{
467 * getDocumentVars
472 final protected function getDocumentVars()
474 $variables = array();
475 foreach ((array)$this->__sleep() as $var) {
476 if (!property_exists($this, $var)) {
477 continue;
479 $variables[$var] = $this->$var;
481 return $variables;
483 // }}}
485 // bool getCurrentSubDocument(array &$document, string $parent_key, array $values, array $past_values) {{{
487 * Generate Sub-document
489 * This method build the difference between the current sub-document,
490 * and the origin one. If there is no difference, it would do nothing,
491 * otherwise it would build a document containing the differences.
493 * @param array &$document Document target
494 * @param string $parent_key Parent key name
495 * @param array $values Current values
496 * @param array $past_values Original values
498 * @return FALSE
500 final function getCurrentSubDocument(&$document, $parent_key, Array $values, Array $past_values)
503 * The current property is a embedded-document,
504 * now we're looking for differences with the
505 * previous value (because we're on an update).
507 * It behaves exactly as getCurrentDocument,
508 * but this is simples (it doesn't support
509 * yet filters)
511 foreach ($values as $key => $value) {
512 $super_key = "{$parent_key}.{$key}";
513 if (is_array($value)) {
515 * Inner document detected
517 if (!array_key_exists($key, $past_values) || !is_array($past_values[$key])) {
519 * We're lucky, it is a new sub-document,
520 * we simple add it
522 $document['$set'][$super_key] = $value;
523 } else {
525 * This is a document like this, we need
526 * to find out the differences to avoid
527 * network overhead.
529 if (!$this->getCurrentSubDocument($document, $super_key, $value, $past_values[$key])) {
530 return FALSE;
533 continue;
534 } else if (!array_key_exists($key, $past_values) || $past_values[$key] !== $value) {
535 $document['$set'][$super_key] = $value;
539 foreach (array_diff(array_keys($past_values), array_keys($values)) as $key) {
540 $super_key = "{$parent_key}.{$key}";
541 $document['$unset'][$super_key] = 1;
544 return TRUE;
546 // }}}
548 // array getCurrentDocument(bool $update) {{{
550 * Get Current Document
552 * Based on this object properties a new document (Array)
553 * is returned. If we're modifying an document, just the modified
554 * properties are included in this document, which uses $set,
555 * $unset, $pushAll and $pullAll.
558 * @param bool $update
560 * @return array
562 final protected function getCurrentDocument($update=FALSE, $current=FALSE)
564 $document = array();
565 $object = $this->getDocumentVars();
567 if (!$current) {
568 $current = (array)$this->_current;
572 $this->findReferences($object);
574 $this->triggerEvent('before_validate', array(&$object, $current));
575 $this->triggerEvent('before_validate_'.($update?'update':'creation'), array(&$object, $current));
577 foreach ($object as $key => $value) {
578 if ($update) {
579 if (is_array($value) && isset($current[$key])) {
581 * If the Field to update is an array, it has a different
582 * behaviour other than $set and $unset. Fist, we need
583 * need to check if it is an array or document, because
584 * they can't be mixed.
587 if (!is_array($current[$key])) {
589 * We're lucky, the field wasn't
590 * an array previously.
592 $this->runFilter($key, $value, $current[$key]);
593 $document['$set'][$key] = $value;
594 continue;
597 if (!$this->getCurrentSubDocument($document, $key, $value, $current[$key])) {
598 throw new Exception("{$key}: Array and documents are not compatible");
600 } else if(!array_key_exists($key, $current) || $value !== $current[$key]) {
602 * It is 'linear' field that has changed, or
603 * has been modified.
605 $past_value = isset($current[$key]) ? $current[$key] : NULL;
606 $this->runFilter($key, $value, $past_value);
607 $document['$set'][$key] = $value;
609 } else {
611 * It is a document insertation, so we
612 * create the document.
614 $this->runFilter($key, $value, NULL);
615 $document[$key] = $value;
619 /* Updated behaves in a diff. way */
620 if ($update) {
621 foreach (array_diff(array_keys($this->_current), array_keys($object)) as $property) {
622 if ($property == '_id') {
623 continue;
625 $document['$unset'][$property] = 1;
629 if (count($document) == 0) {
630 return array();
633 $this->triggerEvent('after_validate', array(&$document));
634 $this->triggerEvent('after_validate_'.($update?'update':'creation'), array(&$object));
636 return $document;
638 // }}}
640 // }}}
642 // EVENT HANDLERS {{{
644 // addEvent($action, $callback) {{{
646 * addEvent
649 final static function addEvent($action, $callback)
651 if (!is_callable($callback)) {
652 throw new ActiveMongo_Exception("Invalid callback");
655 $class = get_called_class();
656 if ($class == __CLASS__ || self::isAbstractChildClass($class)) {
657 $events = & self::$_super_events;
658 } else {
659 $events = & self::$_events[$class];
661 if (!isset($events[$action])) {
662 $events[$action] = array();
664 $events[$action][] = $callback;
665 return TRUE;
667 // }}}
669 // triggerEvent(string $event, Array $events_params) {{{
670 final function triggerEvent($event, Array $events_params = array(), $context=NULL)
672 if (!$context){
673 if (!isset($this)) {
674 $class = get_called_class();
675 $obj = $class;
676 } else {
677 $class = get_class($this);
678 $obj = $this;
680 } else {
681 $class = $context;
682 $obj = $context;
684 $events = & self::$_events[$class][$event];
685 $sevents = & self::$_super_events[$event];
687 /* Super-Events handler receives the ActiveMongo class name as first param */
688 $sevents_params = array_merge(array($class), $events_params);
690 foreach (array('events', 'sevents') as $event_type) {
691 if (count($$event_type) > 0) {
692 $params = "{$event_type}_params";
693 foreach ($$event_type as $fnc) {
694 if (call_user_func_array($fnc, $$params) === FALSE) {
695 return;
701 switch ($event) {
702 case 'before_create':
703 case 'before_update':
704 case 'before_validate':
705 case 'before_delete':
706 case 'before_drop':
707 case 'before_query':
708 case 'after_create':
709 case 'after_update':
710 case 'after_validate':
711 case 'after_delete':
712 case 'after_drop':
713 case 'after_query':
714 $fnc = array($obj, $event);
715 $params = "events_params";
716 if (is_callable($fnc)) {
717 call_user_func_array($fnc, $$params);
719 break;
722 // }}}
724 // void runFilter(string $key, mixed &$value, mixed $past_value) {{{
726 * *Internal Method*
728 * This method check if the current document property has
729 * a filter method, if so, call it.
731 * If the filter returns FALSE, throw an Exception.
733 * @return void
735 protected function runFilter($key, &$value, $past_value)
737 $filter = array($this, "{$key}_filter");
738 if (is_callable($filter)) {
739 $filter = call_user_func_array($filter, array(&$value, $past_value));
740 if ($filter===FALSE) {
741 throw new ActiveMongo_FilterException("{$key} filter failed");
743 $this->$key = $value;
746 // }}}
748 // }}}
750 // void setCursor(MongoCursor $obj) {{{
752 * Set Cursor
754 * This method receive a MongoCursor and make
755 * it iterable.
757 * @param MongoCursor $obj
759 * @return void
761 final protected function setCursor(MongoCursor $obj)
763 $this->_cursor = $obj;
764 $obj->reset();
765 $this->setResult($obj->getNext());
767 // }}}
769 // void setResult(Array $obj) {{{
771 * Set Result
773 * This method takes an document and copy it
774 * as properties in this object.
776 * @param Array $obj
778 * @return void
780 final protected function setResult($obj)
782 /* Unsetting previous results, if any */
783 foreach (array_keys(get_document_vars($this, FALSE)) as $key) {
784 unset($this->$key);
786 $this->_id = NULL;
788 /* Add our current resultset as our object's property */
789 foreach ((array)$obj as $key => $value) {
790 if ($key[0] == '$') {
791 continue;
793 $this->$key = $value;
796 /* Save our record */
797 $this->_current = $obj;
799 // }}}
801 // this find([$_id], [$fields], [$use_document_vars]) {{{
803 * find supports 4 modes of operation.
805 * MODE 1:
806 * $_id = null
807 * Will search using the current document values assigned to this object
809 * MODE 2:
810 * $_id = a non-array value
811 * Will search for a document with matching ID
813 * MODE 3:
814 * $_id = a simple list (non-associative)
815 * Will search for all document with matching IDs
817 * MODE 4:
818 * $_id = an associative array
819 * Will use the array as the template
821 * $fields can be used to limit the return value to only certain fields
823 * By default, the document values are used for the search only in
824 * mode 1, but this can be changed by setting $use_document_vars to
825 * something other than null
827 * Really simple find, which uses this object properties
828 * for fast filtering
830 * @return object this
832 final function find($_id = NULL, $fields = null, $use_document_vars = NULL)
834 return $this->_find($_id, $fields, $use_document_vars, false);
836 // }}}
838 // this findOne([$_id], [$fields], [$use_document_vars]) {{{
840 * See documentation for find()
842 * @return object this
844 final function findOne($_id = NULL, $fields = null, $use_document_vars = NULL)
846 return $this->_find($_id, $fields, $use_document_vars, false);
848 // }}}
850 // mixed findOneValue($field, [$_id], [$use_document_vars]) {{{
852 * $field = the name of the field to return the value of
854 * For $_id and $use_document_vars, see documentation for find()
856 * @return mixed (value of the field)
858 final function findOneValue($field, $_id = NULL, $use_document_vars = NULL)
860 $this->findOne($_id, array($field), $use_document_vars);
862 // return the field value, or null if the record doesn't have it
863 if (isset($this[$field]))
865 return $this[$field];
867 else
869 return NULL;
872 // }}}
874 // array findOneAssoc([$_id], [$fields], [$use_document_vars]) {{{
876 * Same as findOne, but returns the results in an associative array
878 * @return array
880 final function findOneAssoc($_id = NULL, $fields = null, $use_document_vars = NULL)
882 return $this->findOne($_id, $fields, $use_document_vars)->getArray();
884 // }}}
886 // object findOneObj([$_id], [$fields], [$use_document_vars]) {{{
888 * Same as findOne, but returns the results in an object (stdClass)
890 * @return object (stdClass)
892 final function findOneObj($_id = NULL, $fields = null, $use_document_vars = NULL)
894 return (object)$this->findOneAssoc($_id, $fields, $use_document_vars);
896 // }}}
898 // array findAll([$_id], [$fields], [$use_document_vars]) {{{
900 * Same as find, but returns a single array with all the results at once.
902 * This should rarely ever be needed. Mostly useful for tasks such as
903 * quickly JSON encoding the whole result.
905 * @return array(this)
907 final function findAll($_id = NULL, $fields = null, $use_document_vars = NULL)
909 $cursor = $this->find($_id, $fields, $use_document_vars);
910 $ret = array();
911 foreach($cursor as $row)
913 $ret[] = $row;
916 return $ret;
918 // }}}
920 // array findAllAssoc([$_id], [$fields], [$use_document_vars]) {{{
922 * Same as findAll, but returns the rows as associative arrays
924 * This should rarely ever be needed. Mostly useful for tasks such as
925 * quickly JSON encoding the whole result.
927 * @return array(array)
929 final function findAllAssoc($_id = NULL, $fields = null, $use_document_vars = NULL)
931 $cursor = $this->find($_id, $fields, $use_document_vars);
932 $ret = array();
933 foreach($cursor as $row)
935 $ret[] = $row->getArray();
938 return $ret;
940 // }}}
942 // array findAllObj([$_id], [$fields], [$use_document_vars]) {{{
944 * Same as findAll, but returns the rows as objects (stdClass)
946 * This should rarely ever be needed. Mostly useful for tasks such as
947 * quickly JSON encoding the whole result.
949 * @return array(stdClass)
951 final function findAllObj($_id = NULL, $fields = null, $use_document_vars = NULL)
953 $cursor = $this->find($_id, $fields, $use_document_vars);
954 $ret = array();
955 foreach($cursor as $row)
957 $ret[] = (object)$row->getArray();
960 return $ret;
962 // }}}
964 // array findPairs($keyfield, $valuefield, [$_id], [$use_document_vars]) {{{
966 * returns an array of key value pairs, using the specified fields
967 * as the key and value. If the key is not unique the returned value
968 * will be the value for any one of the keys.
970 * If rows that do not contain a value in the key field will not be returned
972 * For $_id and $use_document_vars, see documentation for find()
974 * @return array
976 final function findPairs($keyfield, $valuefield, $_id = NULL, $use_document_vars = NULL)
978 $cursor = $this->find($_id, array($keyfield, $valuefield), $use_document_vars);
979 $ret = array();
980 foreach($cursor as $row)
982 if (isset($row[$keyfield]))
984 if (isset($row[$valuefield]))
986 $ret[] = $row[$valuefield];
988 else
990 $ret[] = NULL;
995 return $ret;
997 // }}}
999 // array findCol($field, [$_id], [$use_document_vars]) {{{
1001 * Same as findOneValue, but returns the the value for multiple rows as an array
1003 * The keys in the array are the record ids
1005 * @return array
1007 final function findCol($field, $_id = NULL, $use_document_vars = NULL)
1009 return $this->findPairs('_id', $field, $_id, $use_document_vars);
1011 // }}}
1013 // this _find([$_id], [$fields], [$use_document_vars], $findOne) {{{
1015 * See documentation for find()
1017 * when $findOne is false, a collection is loaded, when true, a single
1018 * result is loaded into this object, otherwise a cursor is loaded.
1020 * @return object this
1022 final function _find($_id = NULL, $fields = null, $use_document_vars = NULL, $findOne = false)
1024 $vars = array();
1026 if ($use_document_vars || ($use_document_vars === NULL && $_id === NULL))
1028 $vars = get_document_vars($this);
1029 $parent_class = __CLASS__;
1030 foreach ($vars as $key => $value) {
1031 if (!$value) {
1032 unset($vars[$key]);
1034 if ($value InstanceOf $parent_class) {
1035 $this->getColumnDeference($vars, $key, $value);
1036 unset($vars[$key]); /* delete old value */
1041 if ($_id !== NULL)
1043 if (is_array($_id))
1045 $search_ids = array();
1046 $loop_count = 0;
1047 foreach($_id as $k => $v)
1049 if ($loop_count != $k)
1051 $search_ids = false;
1053 if (is_numeric($k))
1055 // This is part of a list of IDs
1056 if (is_array($search_ids)) // true unless we found a reson to treat it differently
1058 $search_ids[] = $v;
1061 else
1064 $vars[$k] = $v;
1066 $loop_count++;
1069 if (is_array($search_ids) && count($search_ids))
1071 $vars['_id'] = array('$in' => $search_ids);
1073 } else {
1074 $vars['_id'] = $_id;
1078 if (!$fields)
1080 // have no fields
1081 $fields = array(); // Mongo expects an array, not NULL
1083 else if (!is_array($fields))
1085 // probably a single field not placed in an array
1086 $fields = array($fields);
1089 if ($findOne)
1091 // single get
1092 $res = $this->_getCollection()->findOne($vars, $fields);
1093 $this->setResult($res);
1095 else
1097 // collection get
1098 $res = $this->_getCollection()->find($vars, $fields);
1099 $this->setCursor($res);
1102 return $this;
1104 // }}}
1106 // void save(bool $async) {{{
1108 * Save
1110 * This method save the current document in MongoDB. If
1111 * we're modifying a document, a update is performed, otherwise
1112 * the document is inserted.
1114 * On updates, special operations such as $set, $pushAll, $pullAll
1115 * and $unset in order to perform efficient updates
1117 * @param bool $async
1119 * @return void
1121 final function save($async=TRUE)
1123 $update = isset($this->_id) && $this->_id InstanceOf MongoID;
1124 $conn = $this->_getCollection();
1125 $document = $this->getCurrentDocument($update);
1126 $object = $this->getDocumentVars();
1128 if (isset($this->_id)) {
1129 $object['_id'] = $this->_id;
1132 if (count($document) == 0) {
1133 return; /*nothing to do */
1136 /* PRE-save hook */
1137 $this->triggerEvent('before_'.($update ? 'update' : 'create'), array(&$document, $object));
1139 if ($update) {
1140 $conn->update(array('_id' => $this->_id), $document, array('safe' => $async));
1141 if (isset($document['$set'])) {
1142 foreach ($document['$set'] as $key => $value) {
1143 if (strpos($key, ".") === FALSE) {
1144 $this->_current[$key] = $value;
1145 $this->$key = $value;
1146 } else {
1147 $keys = explode(".", $key);
1148 $key = $keys[0];
1149 $arr = & $this->$key;
1150 $arrc = & $this->_current[$key];
1151 for ($i=1; $i < count($keys)-1; $i++) {
1152 $arr = &$arr[$keys[$i]];
1153 $arrc = &$arrc[$keys[$i]];
1155 $arr [ $keys[$i] ] = $value;
1156 $arrc[ $keys[$i] ] = $value;
1160 if (isset($document['$unset'])) {
1161 foreach ($document['$unset'] as $key => $value) {
1162 if (strpos($key, ".") === FALSE) {
1163 unset($this->_current[$key]);
1164 unset($this->$key);
1165 } else {
1166 $keys = explode(".", $key);
1167 $key = $keys[0];
1168 $arr = & $this->$key;
1169 $arrc = & $this->_current[$key];
1170 for ($i=1; $i < count($keys)-1; $i++) {
1171 $arr = &$arr[$keys[$i]];
1172 $arrc = &$arrc[$keys[$i]];
1174 unset($arr [ $keys[$i] ]);
1175 unset($arrc[ $keys[$i] ]);
1179 } else {
1180 $conn->insert($document, $async);
1181 $this->setResult($document);
1184 $this->triggerEvent('after_'.($update ? 'update' : 'create'), array($document, $object));
1186 return TRUE;
1188 // }}}
1190 // bool delete() {{{
1192 * Delete the current document
1194 * @return bool
1196 final function delete()
1199 $document = array('_id' => $this->_id);
1200 if ($this->_cursor InstanceOf MongoCursor) {
1201 $this->triggerEvent('before_delete', array($document));
1202 $result = $this->_getCollection()->remove($document);
1203 $this->triggerEvent('after_delete', array($document));
1204 $this->setResult(array());
1205 return $result;
1207 else if ($this->_cursor_ex == self::FIND_AND_MODIFY &&
1208 !is_null($this->_cursor_ex_value) &&
1209 $this->_cursor_ex_value['ok'] == 1)
1211 // delete by ID
1212 $this->triggerEvent('before_delete', array($document));
1213 $result = $this->_getCollection()->remove($document);
1214 $this->triggerEvent('after_delete', array($document));
1215 $this->setResult(array());
1216 return $result;
1217 } else {
1218 /* remove */
1219 $this->triggerEvent('before_delete', array($document));
1220 $this->_getCollection()->remove($criteria);
1221 $this->triggerEvent('after_delete', array($document));
1223 /* reset object */
1224 $this->reset();
1226 return TRUE;
1228 return FALSE;
1230 // }}}
1232 // Update {{{
1234 * Multiple updates.
1236 * This method perform multiple updates when a given
1237 * criteria matchs (using where).
1239 * By default the update is perform safely, but it can be
1240 * changed.
1242 * After the operation is done, the criteria is deleted.
1244 * @param array $value Values to set
1245 * @param bool $safe Whether or not peform the operation safely
1247 * @return bool
1250 function update(Array $value, $safe=TRUE)
1252 $this->_assertNotInQuery();
1254 $criteria = (array) $this->_query;
1255 $options = array('multiple' => TRUE, 'safe' => $safe);
1257 /* update */
1258 $col = $this->_getCollection();
1259 $col->update($criteria, array('$set' => $value), $options);
1261 /* reset object */
1262 $this->reset();
1264 return TRUE;
1266 // }}}
1268 // void drop() {{{
1270 * Delete the current colleciton and all its documents
1272 * @return void
1274 final static function drop()
1276 $class = get_called_class();
1277 if ($class == __CLASS__ || self::isAbstractChildClass($class)) {
1278 return FALSE;
1280 $obj = new $class;
1281 $obj->triggerEvent('before_drop');
1282 $result = $obj->_getCollection()->drop();
1283 $obj->triggerEvent('after_drop');
1284 if ($result['ok'] != 1) {
1285 throw new ActiveMongo_Exception($result['errmsg']);
1287 return TRUE;
1290 // }}}
1292 // int count() {{{
1294 * Return the number of documents in the actual request. If
1295 * we're not in a request, it will return 0.
1297 * @return int
1299 final function count()
1301 if ($this->valid()) {
1302 return $this->_cursor->count();
1304 return 0;
1306 // }}}
1308 // void setup() {{{
1310 * This method should contain all the indexes, and shard keys
1311 * needed by the current collection. This try to make
1312 * installation on development environments easier.
1314 function setup()
1317 // }}}
1319 // batchInsert {{{
1320 /**
1321 * Perform a batchInsert of objects.
1323 * @param array $documents Arrays of documents to insert
1324 * @param bool $safe True if a safe will be performed, this means data validation, and wait for MongoDB OK reply
1325 * @param bool $on_error_continue If an error happen while validating an object, if it should continue or not
1327 * @return bool
1329 final public static function batchInsert(Array $documents, $safe=TRUE, $on_error_continue=TRUE)
1331 $context = get_called_class();
1333 if (__CLASS__ == $context || self::isAbstractChildClass($context)) {
1334 throw new ActiveMongo_Exception("Invalid batchInsert usage");
1338 if ($safe) {
1339 foreach ($documents as $id => $doc) {
1340 $valid = FALSE;
1341 if (is_array($doc)) {
1342 try {
1343 self::triggerEvent('before_create', array(&$doc), $context);
1344 self::triggerEvent('before_validate', array(&$doc, $doc), $context);
1345 self::triggerEvent('before_validate_creation', array(&$doc, $doc), $context);
1346 $documents[$id] = $doc;
1347 $valid = TRUE;
1348 } catch (Exception $e) {}
1350 if (!$valid) {
1351 if (!$on_error_continue) {
1352 throw new ActiveMongo_FilterException("Document $id is invalid");
1354 unset($documents[$id]);
1359 return self::_getCollection()->batchInsert($documents, array("safe" => $safe));
1361 // }}}
1363 // bool addIndex(array $columns, array $options) {{{
1365 * addIndex
1367 * Create an Index in the current collection.
1369 * @param array $columns L ist of columns
1370 * @param array $options Options
1372 * @return bool
1374 final function addIndex($columns, $options=array())
1376 $default_options = array(
1377 'background' => 1,
1380 if (!is_array($columns)) {
1381 $columns = array($columns => 1);
1384 foreach ($columns as $id => $name) {
1385 if (is_numeric($id)) {
1386 unset($columns[$id]);
1387 $columns[$name] = 1;
1391 foreach ($default_options as $option => $value) {
1392 if (!isset($options[$option])) {
1393 $options[$option] = $value;
1397 $collection = $this->_getCollection();
1399 return $collection->ensureIndex($columns, $options);
1401 // }}}
1403 // Array getIndexes() {{{
1405 * Return an array with all indexes
1407 * @return array
1409 final static function getIndexes()
1411 return self::_getCollection()->getIndexInfo();
1413 // }}}
1415 // string __toString() {{{
1417 * To String
1419 * If this object is treated as a string,
1420 * it would return its ID.
1422 * @return string
1424 function __toString()
1426 return (string)$this->getID();
1428 // }}}
1430 // array sendCmd(array $cmd) {{{
1432 * This method sends a command to the current
1433 * database.
1435 * @param array $cmd Current command
1437 * @return array
1439 final protected function sendCmd($cmd)
1441 return $this->_getConnection()->command($cmd);
1443 // }}}
1445 // ITERATOR {{{
1447 // array getArray() {{{
1449 * Return the current document as an array
1450 * instead of a ActiveMongo object
1452 * @return Array
1454 final function getArray()
1456 return get_document_vars($this);
1458 // }}}
1460 // void reset() {{{
1462 * Reset our Object, delete the current cursor if any, and reset
1463 * unsets the values.
1465 * @return void
1467 final function reset()
1469 if ($this->_cloned) {
1470 throw new ActiveMongo_Exception("Cloned objects can't be reseted");
1472 $this->_properties = NULL;
1473 $this->_cursor = NULL;
1474 $this->_cursor_ex = NULL;
1475 $this->_query = NULL;
1476 $this->_sort = NULL;
1477 $this->_limit = 0;
1478 $this->_skip = 0;
1479 $this->setResult(array());
1481 // }}}
1483 // bool valid() {{{
1485 * Valid
1487 * Return if we're on an iteration and if it is still valid
1489 * @return TRUE
1491 final function valid()
1493 $valid = FALSE;
1494 if (!$this->_cursor_ex) {
1495 if (!$this->_cursor InstanceOf MongoCursor) {
1496 $this->doQuery();
1498 $valid = $this->_cursor InstanceOf MongoCursor && $this->_cursor->valid();
1499 } else {
1500 switch ($this->_cursor_ex) {
1501 case self::FIND_AND_MODIFY:
1502 if ($this->_limit > $this->_findandmodify_cnt) {
1503 $this->_execFindAndModify();
1504 $valid = $this->_cursor_ex_value['ok'] == 1;
1506 break;
1507 default:
1508 throw new ActiveMongo_Exception("Invalid _cursor_ex value");
1512 return $valid;
1514 // }}}
1516 // bool next() {{{
1518 * Move to the next document
1520 * @return bool
1522 final function next()
1524 if ($this->_cloned) {
1525 throw new ActiveMongo_Exception("Cloned objects can't iterate");
1527 if (!$this->_cursor_ex) {
1528 $result = $this->_cursor->next();
1529 $this->current();
1530 return $result;
1531 } else {
1532 switch ($this->_cursor_ex) {
1533 case self::FIND_AND_MODIFY:
1534 $this->_cursor_ex_value = NULL;
1535 break;
1536 default:
1537 throw new ActiveMongo_Exception("Invalid _cursor_ex value");
1541 // }}}
1543 // this current() {{{
1545 * Return the current object, and load the current document
1546 * as this object property
1548 * @return object
1550 final function current()
1552 if (!$this->_cursor_ex) {
1553 $this->setResult($this->_cursor->current());
1554 } else {
1555 switch ($this->_cursor_ex) {
1556 case self::FIND_AND_MODIFY:
1557 if (count($this->_cursor_ex_value) == 0) {
1558 $this->_execFindAndModify();
1560 $this->setResult($this->_cursor_ex_value['value']);
1561 break;
1562 default:
1563 throw new ActiveMongo_Exception("Invalid _cursor_ex value");
1566 return $this;
1568 // }}}
1570 // bool rewind() {{{
1572 * Go to the first document
1574 final function rewind()
1576 if ($this->_cloned) {
1577 throw new ActiveMongo_Exception("Cloned objects can't iterate");
1579 if (!$this->_cursor_ex) {
1580 /* rely on MongoDB cursor */
1581 if (!$this->_cursor InstanceOf MongoCursor) {
1582 $this->doQuery();
1584 $result = $this->_cursor->rewind();
1585 $this->current();
1586 return $result;
1587 } else {
1588 switch ($this->_cursor_ex) {
1589 case self::FIND_AND_MODIFY:
1590 $this->_findandmodify_cnt = 0;
1591 break;
1592 default:
1593 throw new ActiveMongo_Exception("Invalid _cursor_ex value");
1597 // }}}
1599 // }}}
1601 // ARRAY ACCESS {{{
1602 final function offsetExists($offset)
1604 return isset($this->$offset);
1607 final function offsetGet($offset)
1609 return $this->$offset;
1612 final function offsetSet($offset, $value)
1614 $this->$offset = $value;
1617 final function offsetUnset($offset)
1619 unset($this->$offset);
1621 // }}}
1623 // REFERENCES {{{
1625 // array getReference() {{{
1627 * ActiveMongo extended the Mongo references, adding
1628 * the concept of 'dynamic' requests, saving in the database
1629 * the current query with its options (sort, limit, etc).
1631 * This is useful to associate a document with a given
1632 * request. To undestand this better please see the 'reference'
1633 * example.
1635 * @return array
1637 final function getReference($dynamic=FALSE)
1639 if (!$this->getID() && !$dynamic) {
1640 return NULL;
1643 $document = array(
1644 '$ref' => $this->CollectionName(),
1645 '$id' => $this->getID(),
1646 '$db' => $this->getDatabaseName(),
1647 'class' => get_class($this),
1650 if ($dynamic) {
1651 if (!$this->_cursor InstanceOf MongoCursor && $this->_cursor_ex === NULL) {
1652 $this->doQuery();
1655 if (!$this->_cursor InstanceOf MongoCursor) {
1656 throw new ActiveMongo_Exception("Only MongoDB native cursor could have dynamic references");
1659 $cursor = $this->_cursor;
1660 if (!is_callable(array($cursor, "Info"))) {
1661 throw new Exception("Please upgrade your PECL/Mongo module to use this feature");
1663 $document['dynamic'] = array();
1664 $query = $cursor->Info();
1665 foreach ($query as $type => $value) {
1666 $document['dynamic'][$type] = $value;
1669 return $document;
1671 // }}}
1673 // void getDocumentReferences($document, &$refs) {{{
1675 * Get Current References
1677 * Inspect the current document trying to get any references,
1678 * if any.
1680 * @param array $document Current document
1681 * @param array &$refs References found in the document.
1682 * @param array $parent_key Parent key
1684 * @return void
1686 final protected function getDocumentReferences($document, &$refs, $parent_key=NULL)
1688 foreach ($document as $key => $value) {
1689 if (is_array($value)) {
1690 if (MongoDBRef::isRef($value)) {
1691 $pkey = $parent_key;
1692 $pkey[] = $key;
1693 $refs[] = array('ref' => $value, 'key' => $pkey);
1694 } else {
1695 $parent_key1 = $parent_key;
1696 $parent_key1[] = $key;
1697 $this->getDocumentReferences($value, $refs, $parent_key1);
1702 // }}}
1704 // object _deferencingCreateObject(string $class) {{{
1706 * Called at deferencig time
1708 * Check if the given string is a class, and it is a sub class
1709 * of ActiveMongo, if it is instance and return the object.
1711 * @param string $class
1713 * @return object
1715 private function _deferencingCreateObject($class)
1717 if (!is_subclass_of($class, __CLASS__)) {
1718 throw new ActiveMongo_Exception("Fatal Error, imposible to create ActiveMongo object of {$class}");
1720 return new $class;
1722 // }}}
1724 // void _deferencingRestoreProperty(array &$document, array $keys, mixed $req) {{{
1726 * Called at deferencig time
1728 * This method iterates $document until it could match $keys path, and
1729 * replace its value by $req.
1731 * @param array &$document Document to replace
1732 * @param array $keys Path of property to change
1733 * @param mixed $req Value to replace.
1735 * @return void
1737 private function _deferencingRestoreProperty(&$document, $keys, $req)
1739 $obj = & $document;
1741 /* find the $req proper spot */
1742 foreach ($keys as $key) {
1743 $obj = & $obj[$key];
1746 $obj = $req;
1748 /* Delete reference variable */
1749 unset($obj);
1751 // }}}
1753 // object _deferencingQuery($request) {{{
1755 * Called at deferencig time
1757 * This method takes a dynamic reference and request
1758 * it to MongoDB.
1760 * @param array $request Dynamic reference
1762 * @return this
1764 private function _deferencingQuery($request)
1766 $collection = $this->_getCollection();
1767 $cursor = $collection->find($request['query'], $request['fields']);
1768 if ($request['limit'] > 0) {
1769 $cursor->limit($request['limit']);
1771 if ($request['skip'] > 0) {
1772 $cursor->skip($request['skip']);
1775 $this->setCursor($cursor);
1777 return $this;
1779 // }}}
1781 // void doDeferencing() {{{
1783 * Perform a deferencing in the current document, if there is
1784 * any reference.
1786 * ActiveMongo will do its best to group references queries as much
1787 * as possible, in order to perform as less request as possible.
1789 * ActiveMongo doesn't rely on MongoDB references, but it can support
1790 * it, but it is prefered to use our referencing.
1792 * @experimental
1794 final function doDeferencing($refs=array())
1796 /* Get current document */
1797 $document = get_document_vars($this);
1799 if (count($refs)==0) {
1800 /* Inspect the whole document */
1801 $this->getDocumentReferences($document, $refs);
1804 $db = $this->_getConnection();
1806 /* Gather information about ActiveMongo Objects
1807 * that we need to create
1809 $classes = array();
1810 foreach ($refs as $ref) {
1811 if (!isset($ref['ref']['class'])) {
1813 /* Support MongoDBRef, we do our best to be compatible {{{ */
1814 /* MongoDB 'normal' reference */
1816 $obj = MongoDBRef::get($db, $ref['ref']);
1818 /* Offset the current document to the right spot */
1819 /* Very inefficient, never use it, instead use ActiveMongo References */
1821 $this->_deferencingRestoreProperty($document, $ref['key'], $obj);
1823 /* Dirty hack, override our current document
1824 * property with the value itself, in order to
1825 * avoid replace a MongoDB reference by its content
1827 $this->_deferencingRestoreProperty($this->_current, $ref['key'], $obj);
1829 /* }}} */
1831 } else {
1833 if (isset($ref['ref']['dynamic'])) {
1834 /* ActiveMongo Dynamic Reference */
1836 /* Create ActiveMongo object */
1837 $req = $this->_deferencingCreateObject($ref['ref']['class']);
1839 /* Restore saved query */
1840 $req->_deferencingQuery($ref['ref']['dynamic']);
1842 $results = array();
1844 /* Add the result set */
1845 foreach ($req as $result) {
1846 $results[] = clone $result;
1849 /* add information about the current reference */
1850 foreach ($ref['ref'] as $key => $value) {
1851 $results[$key] = $value;
1854 $this->_deferencingRestoreProperty($document, $ref['key'], $results);
1856 } else {
1857 /* ActiveMongo Reference FTW! */
1858 $classes[$ref['ref']['class']][] = $ref;
1863 /* {{{ Create needed objects to query MongoDB and replace
1864 * our references by its objects documents.
1866 foreach ($classes as $class => $refs) {
1867 $req = $this->_deferencingCreateObject($class);
1869 /* Load list of IDs */
1870 $ids = array();
1871 foreach ($refs as $ref) {
1872 $ids[] = $ref['ref']['$id'];
1875 /* Search to MongoDB once for all IDs found */
1876 $req->find($ids);
1879 /* Replace our references by its objects */
1880 foreach ($refs as $ref) {
1881 $id = $ref['ref']['$id'];
1882 $place = $ref['key'];
1883 foreach ($req as $item) {
1884 if ($item->getID() == $id) {
1885 $this->_deferencingRestoreProperty($document, $place, clone $req);
1888 unset($obj);
1891 /* Release request, remember we
1892 * safely cloned it,
1894 unset($req);
1896 // }}}
1898 /* Replace the current document by the new deferenced objects */
1899 foreach ($document as $key => $value) {
1900 $this->$key = $value;
1903 // }}}
1905 // void getColumnDeference(&$document, $propety, ActiveMongo Obj) {{{
1907 * Prepare a "selector" document to search treaing the property
1908 * as a reference to the given ActiveMongo object.
1911 final function getColumnDeference(&$document, $property, ActiveMongo $obj)
1913 $document["{$property}.\$id"] = $obj->getID();
1915 // }}}
1917 // void findReferences(&$document) {{{
1919 * Check if in the current document to insert or update
1920 * exists any references to other ActiveMongo Objects.
1922 * @return void
1924 final function findReferences(&$document)
1926 if (!is_array($document)) {
1927 return;
1929 foreach($document as &$value) {
1930 $parent_class = __CLASS__;
1931 if (is_array($value)) {
1932 if (MongoDBRef::isRef($value)) {
1933 /* If the property we're inspecting is a reference,
1934 * we need to remove the values, restoring the valid
1935 * Reference.
1937 $arr = array(
1938 '$ref'=>1, '$id'=>1, '$db'=>1, 'class'=>1, 'dynamic'=>1
1940 foreach (array_keys($value) as $key) {
1941 if (!isset($arr[$key])) {
1942 unset($value[$key]);
1945 } else {
1946 $this->findReferences($value);
1948 } else if ($value InstanceOf $parent_class) {
1949 $value = $value->getReference();
1952 /* trick: delete last var. reference */
1953 unset($value);
1955 // }}}
1957 // void __clone() {{{
1958 /**
1959 * Cloned objects are rarely used, but ActiveMongo
1960 * uses it to create different objects per everyrecord,
1961 * which is used at deferencing. Therefore cloned object
1962 * do not contains the recordset, just the actual document,
1963 * so iterations are not allowed.
1966 final function __clone()
1968 if (!$this->_current) {
1969 throw new ActiveMongo_Exception("Empty objects can't be cloned");
1971 unset($this->_cursor);
1972 $this->_cloned = TRUE;
1974 // }}}
1976 // }}}
1978 // GET DOCUMENT ID {{{
1980 // getID() {{{
1982 * Return the current document ID. If there is
1983 * no document it would return FALSE.
1985 * @return object|FALSE
1987 final public function getID()
1989 if ($this->_id instanceof MongoID) {
1990 return $this->_id;
1992 return FALSE;
1994 // }}}
1996 // string key() {{{
1998 * Return the current key
2000 * @return string
2002 final function key()
2004 return (string)$this->getID();
2006 // }}}
2008 // }}}
2010 // Fancy (and silly) query abstraction {{{
2012 // _assertNotInQuery() {{{
2014 * Check if we can modify the query or not. We cannot modify
2015 * the query if we're iterating over and oldest query, in this case the
2016 * object must be reset.
2018 * @return void
2020 final private function _assertNotInQuery()
2022 if ($this->_cloned || $this->_cursor InstanceOf MongoCursor || $this->_cursor_ex != NULL) {
2023 throw new ActiveMongo_Exception("You cannot modify the query, please reset the object");
2026 // }}}
2028 // bool servedFromCache() {{{
2030 * Return True if the current result
2031 * was provided by a before_query hook (aka cache)
2032 * or False if it was retrieved from MongoDB
2034 * @return bool
2036 final function servedFromCache()
2038 return $this->_cached;
2040 // }}}
2042 // doQuery() {{{
2044 * Build the current request and send it to MongoDB.
2046 * @return this
2048 final function doQuery($use_cache=TRUE)
2050 if ($this->_cursor_ex) {
2051 switch ($this->_cursor_ex) {
2052 case self::FIND_AND_MODIFY:
2053 $this->_cursor_ex_value = NULL;
2054 return;
2055 default:
2056 throw new ActiveMongo_Exception("Invalid _cursor_ex value");
2059 $this->_assertNotInQuery();
2061 $query = array(
2062 'collection' => $this->CollectionName(),
2063 'query' => (array)$this->_query,
2064 'properties' => (array)$this->_properties,
2065 'sort' => (array)$this->_sort,
2066 'skip' => $this->_skip,
2067 'limit' => $this->_limit
2070 $this->_cached = FALSE;
2072 self::triggerEvent('before_query', array(&$query, &$documents, $use_cache));
2074 if ($documents InstanceOf MongoCursor && $use_cache) {
2075 $this->_cached = TRUE;
2076 $this->setCursor($documents);
2077 return $this;
2080 $col = $this->_getCollection();
2081 if (count($query['properties']) > 0) {
2082 $cursor = $col->find($query['query'], $query['properties']);
2083 } else {
2084 $cursor = $col->find($query['query']);
2086 if (count($query['sort']) > 0) {
2087 $cursor->sort($query['sort']);
2089 if ($query['limit'] > 0) {
2090 $cursor->limit($query['limit']);
2092 if ($query['skip'] > 0) {
2093 $cursor->skip($query['skip']);
2096 self::triggerEvent('after_query', array($query, $cursor));
2098 /* Our cursor must be sent to ActiveMongo */
2099 $this->setCursor($cursor);
2101 return $this;
2103 // }}}
2105 // properties($props) {{{
2107 * Select 'properties' or 'columns' to be included in the document,
2108 * by default all properties are included.
2110 * @param array $props
2112 * @return this
2114 final function properties($props)
2116 $this->_assertNotInQuery();
2118 if (!is_array($props) && !is_string($props)) {
2119 return FALSE;
2122 if (is_string($props)) {
2123 $props = explode(",", $props);
2126 foreach ($props as $id => $name) {
2127 $props[trim($name)] = 1;
2128 unset($props[$id]);
2132 /* _id should always be included */
2133 $props['_id'] = 1;
2135 $this->_properties = $props;
2137 return $this;
2140 final function columns($properties)
2142 return $this->properties($properties);
2144 // }}}
2146 // where($property, $value) {{{
2148 * Where abstraction.
2151 final function where($property_str, $value=NULL)
2153 $this->_assertNotInQuery();
2155 if (is_array($property_str)) {
2156 if ($value != NULL) {
2157 throw new ActiveMongo_Exception("Invalid parameters");
2159 foreach ($property_str as $property => $value) {
2160 if (is_numeric($property)) {
2161 $property = $value;
2162 $value = NULL;
2164 $this->where($property, $value);
2166 return $this;
2169 $column = explode(" ", trim($property_str));
2170 if (count($column) != 1 && count($column) != 2) {
2171 throw new ActiveMongo_Exception("Failed while parsing '{$property_str}'");
2172 } else if (count($column) == 2) {
2174 $exp_scalar = TRUE;
2175 switch (strtolower($column[1])) {
2176 case '>':
2177 case '$gt':
2178 $op = '$gt';
2179 break;
2181 case '>=':
2182 case '$gte':
2183 $op = '$gte';
2184 break;
2186 case '<':
2187 case '$lt':
2188 $op = '$lt';
2189 break;
2191 case '<=':
2192 case '$lte':
2193 $op = '$lte';
2194 break;
2196 case '==':
2197 case '$eq':
2198 case '=':
2199 if (is_array($value)) {
2200 $op = '$all';
2201 $exp_scalar = FALSE;
2202 } else {
2203 $op = NULL;
2205 break;
2207 case '!=':
2208 case '<>':
2209 case '$ne':
2210 if (is_array($value)) {
2211 $op = '$nin';
2212 $exp_scalar = FALSE;
2213 } else {
2214 $op = '$ne';
2216 break;
2218 case '%':
2219 case 'mod':
2220 case '$mod':
2221 $op = '$mod';
2222 $exp_scalar = FALSE;
2223 break;
2225 case 'exists':
2226 case '$exists':
2227 if ($value === NULL) {
2228 $value = 1;
2230 $op = '$exists';
2231 break;
2233 /* regexp */
2234 case 'regexp':
2235 case 'regex':
2236 $value = new MongoRegex($value);
2237 $op = NULL;
2238 break;
2240 /* arrays */
2241 case 'in':
2242 case '$in':
2243 $exp_scalar = FALSE;
2244 $op = '$in';
2245 break;
2247 case '$nin':
2248 case 'nin':
2249 $exp_scalar = FALSE;
2250 $op = '$nin';
2251 break;
2254 /* geo operations */
2255 case 'near':
2256 case '$near':
2257 $op = '$near';
2258 $exp_scalar = FALSE;
2259 break;
2261 default:
2262 throw new ActiveMongo_Exception("Failed to parse '{$column[1]}'");
2265 if ($exp_scalar && is_array($value)) {
2266 throw new ActiveMongo_Exception("Cannot use comparing operations with Array");
2267 } else if (!$exp_scalar && !is_array($value)) {
2268 throw new ActiveMongo_Exception("The operation {$column[1]} expected an Array");
2271 if ($op) {
2272 $value = array($op => $value);
2274 } else if (is_array($value)) {
2275 $value = array('$in' => $value);
2278 $spot = & $this->_query[$column[0]];
2279 if (is_array($spot) && is_array($value)) {
2280 $spot[key($value)] = current($value);
2281 } else {
2282 /* simulate AND among same properties if
2283 * multiple values is passed for same property
2285 if (isset($spot)) {
2286 if (is_array($spot)) {
2287 $spot['$all'][] = $value;
2288 } else {
2289 $spot = array('$all' => array($spot, $value));
2291 } else {
2292 $spot = $value;
2296 return $this;
2298 // }}}
2300 // sort($sort_str) {{{
2302 * Abstract the documents sorting.
2304 * @param string $sort_str List of properties to use as sorting
2306 * @return this
2308 final function sort($sort_str)
2310 $this->_assertNotInQuery();
2312 $this->_sort = array();
2313 foreach ((array)explode(",", $sort_str) as $sort_part_str) {
2314 $sort_part = explode(" ", trim($sort_part_str), 2);
2315 switch(count($sort_part)) {
2316 case 1:
2317 $sort_part[1] = 'ASC';
2318 break;
2319 case 2:
2320 break;
2321 default:
2322 throw new ActiveMongo_Exception("Don't know how to parse {$sort_part_str}");
2325 /* Columns name can't be empty */
2326 if (!trim($sort_part[0])) {
2327 throw new ActiveMongo_Exception("Don't know how to parse {$sort_part_str}");
2330 switch (strtoupper($sort_part[1])) {
2331 case 'ASC':
2332 $sort_part[1] = 1;
2333 break;
2334 case 'DESC':
2335 $sort_part[1] = -1;
2336 break;
2337 default:
2338 throw new ActiveMongo_Exception("Invalid sorting direction `{$sort_part[1]}`");
2340 $this->_sort[ $sort_part[0] ] = $sort_part[1];
2343 return $this;
2345 // }}}
2347 // limit($limit, $skip) {{{
2349 * Abstract the limitation and pagination of documents.
2351 * @param int $limit Number of max. documents to retrieve
2352 * @param int $skip Number of documents to skip
2354 * @return this
2356 final function limit($limit=0, $skip=0)
2358 $this->_assertNotInQuery();
2360 if ($limit < 0 || $skip < 0) {
2361 return FALSE;
2363 $this->_limit = $limit;
2364 $this->_skip = $skip;
2366 return $this;
2368 // }}}
2370 // FindAndModify(Array $document) {{{
2372 * findAndModify
2376 final function findAndModify($document)
2378 $this->_assertNotInQuery();
2380 if (count($document) === 0) {
2381 throw new ActiveMongo_Exception("Empty \$document is not allowed");
2384 $this->_cursor_ex = self::FIND_AND_MODIFY;
2385 $this->_findandmodify = $document;
2387 return $this;
2390 private function _execFindAndModify()
2392 $query = (array)$this->_query;
2394 $query = array(
2395 "findandmodify" => $this->CollectionName(),
2396 "query" => $query,
2397 "update" => array('$set' => $this->_findandmodify),
2398 "new" => TRUE,
2400 if (isset($this->_sort)) {
2401 $query["sort"] = $this->_sort;
2403 $this->_cursor_ex_value = $this->sendCMD($query);
2405 $this->_findandmodify_cnt++;
2407 // }}}
2409 // }}}
2411 // __sleep() {{{
2413 * Return a list of properties to serialize, to save
2414 * into MongoDB
2416 * @return array
2418 function __sleep()
2420 return array_keys(get_document_vars($this));
2422 // }}}
2426 require_once dirname(__FILE__)."/Validators.php";
2427 require_once dirname(__FILE__)."/Exceptions.php";
2430 * Local variables:
2431 * tab-width: 4
2432 * c-basic-offset: 4
2433 * End:
2434 * vim600: sw=4 ts=4 fdm=marker
2435 * vim<600: sw=4 ts=4