- fixed potential bug with properties(), from now on _id propety is included in all...
[activemongo.git] / lib / ActiveMongo.php
blob1917fe2b11ffa320fc1396fa3cbbb4c1e71705c1
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 $_cached = FALSE;
164 private $_query = NULL;
165 private $_sort = NULL;
166 private $_limit = 0;
167 private $_skip = 0;
168 private $_properties = NULL;
169 /* }}} */
172 * Current document ID
174 * @type MongoID
176 private $_id;
179 * Tell if the current object
180 * is cloned or not.
182 * @type bool
184 private $_cloned = FALSE;
185 // }}}
187 // GET CONNECTION CONFIG {{{
189 // string getCollectionName() {{{
191 * Get Collection Name, by default the class name,
192 * but you it can be override at the class itself to give
193 * a custom name.
195 * @return string Collection Name
197 protected function getCollectionName()
199 if (isset($this)) {
200 return strtolower(get_class($this));
201 } else {
202 return strtolower(get_called_class());
205 // }}}
207 // string getDatabaseName() {{{
209 * Get Database Name, by default it is used
210 * the db name set by ActiveMong::connect()
212 * @return string DB Name
214 protected function getDatabaseName()
216 if (is_NULL(self::$_db)) {
217 throw new ActiveMongo_Exception("There is no information about the default DB name");
219 return self::$_db;
221 // }}}
223 // void install() {{{
225 * Install.
227 * This static method iterate over the classes lists,
228 * and execute the setup() method on every ActiveMongo
229 * subclass. You should do this just once.
232 final public static function install()
234 $classes = array_reverse(get_declared_classes());
235 foreach ($classes as $class)
237 if ($class == __CLASS__) {
238 break;
240 if (is_subclass_of($class, __CLASS__)) {
241 $obj = new $class;
242 $obj->setup();
246 // }}}
248 // void connection($db, $host) {{{
250 * Connect
252 * This method setup parameters to connect to a MongoDB
253 * database. The connection is done when it is needed.
255 * @param string $db Database name
256 * @param string $host Host to connect
257 * @param string $user User (Auth)
258 * @param string $pwd Password (Auth)
260 * @return void
262 final public static function connect($db, $host='localhost', $user = NULL, $pwd=NULL)
264 self::$_host = $host;
265 self::$_db = $db;
266 self::$_user = $user;
267 self::$_pwd = $pwd;
269 // }}}
271 // MongoConnection _getConnection() {{{
273 * Get Connection
275 * Get a valid database connection
277 * @return MongoConnection
279 final protected function _getConnection()
281 if (is_NULL(self::$_conn)) {
282 if (is_NULL(self::$_host)) {
283 self::$_host = 'localhost';
285 self::$_conn = new Mongo(self::$_host);
287 if (isset($this)) {
288 $dbname = $this->getDatabaseName();
289 } else {
290 $dbname = self::getDatabaseName();
292 if (!isSet(self::$_dbs[$dbname])) {
293 self::$_dbs[$dbname] = self::$_conn->selectDB($dbname);
295 if ( !is_NULL(self::$_user ) && !is_NULL(self::$_pwd ) ) {
296 self::$_dbs[$dbname]->authenticate(self::$_user,self::$_pwd);
300 return self::$_dbs[$dbname];
302 // }}}
304 // MongoCollection _getCollection() {{{
306 * Get Collection
308 * Get a collection connection.
310 * @return MongoCollection
312 final protected function _getCollection()
314 if (isset($this)) {
315 $colName = $this->getCollectionName();
316 } else {
317 $colName = self::getCollectionName();
319 if (!isset(self::$_collections[$colName])) {
320 self::$_collections[$colName] = self::_getConnection()->selectCollection($colName);
322 return self::$_collections[$colName];
324 // }}}
326 // }}}
328 // GET DOCUMENT TO SAVE OR UPDATE {{{
330 // getDocumentVars() {{{
332 * getDocumentVars
337 final protected function getDocumentVars()
339 $variables = array();
340 foreach ((array)$this->__sleep() as $var) {
341 if (!property_exists($this, $var)) {
342 continue;
344 $variables[$var] = $this->$var;
346 return $variables;
348 // }}}
350 // bool getCurrentSubDocument(array &$document, string $parent_key, array $values, array $past_values) {{{
352 * Generate Sub-document
354 * This method build the difference between the current sub-document,
355 * and the origin one. If there is no difference, it would do nothing,
356 * otherwise it would build a document containing the differences.
358 * @param array &$document Document target
359 * @param string $parent_key Parent key name
360 * @param array $values Current values
361 * @param array $past_values Original values
363 * @return FALSE
365 final function getCurrentSubDocument(&$document, $parent_key, Array $values, Array $past_values)
368 * The current property is a embedded-document,
369 * now we're looking for differences with the
370 * previous value (because we're on an update).
372 * It behaves exactly as getCurrentDocument,
373 * but this is simples (it doesn't support
374 * yet filters)
376 foreach ($values as $key => $value) {
377 $super_key = "{$parent_key}.{$key}";
378 if (is_array($value)) {
380 * Inner document detected
382 if (!array_key_exists($key, $past_values) || !is_array($past_values[$key])) {
384 * We're lucky, it is a new sub-document,
385 * we simple add it
387 $document['$set'][$super_key] = $value;
388 } else {
390 * This is a document like this, we need
391 * to find out the differences to avoid
392 * network overhead.
394 if (!$this->getCurrentSubDocument($document, $super_key, $value, $past_values[$key])) {
395 return FALSE;
398 continue;
399 } else if (!array_key_exists($key, $past_values) || $past_values[$key] !== $value) {
400 $document['$set'][$super_key] = $value;
404 foreach (array_diff(array_keys($past_values), array_keys($values)) as $key) {
405 $super_key = "{$parent_key}.{$key}";
406 $document['$unset'][$super_key] = 1;
409 return TRUE;
411 // }}}
413 // array getCurrentDocument(bool $update) {{{
415 * Get Current Document
417 * Based on this object properties a new document (Array)
418 * is returned. If we're modifying an document, just the modified
419 * properties are included in this document, which uses $set,
420 * $unset, $pushAll and $pullAll.
423 * @param bool $update
425 * @return array
427 final protected function getCurrentDocument($update=FALSE, $current=FALSE)
429 $document = array();
430 $object = $this->getDocumentVars();
432 if (!$current) {
433 $current = (array)$this->_current;
437 $this->findReferences($object);
439 $this->triggerEvent('before_validate', array(&$object, $current));
440 $this->triggerEvent('before_validate_'.($update?'update':'creation'), array(&$object, $current));
442 foreach ($object as $key => $value) {
443 if ($update) {
444 if (is_array($value) && isset($current[$key])) {
446 * If the Field to update is an array, it has a different
447 * behaviour other than $set and $unset. Fist, we need
448 * need to check if it is an array or document, because
449 * they can't be mixed.
452 if (!is_array($current[$key])) {
454 * We're lucky, the field wasn't
455 * an array previously.
457 $this->runFilter($key, $value, $current[$key]);
458 $document['$set'][$key] = $value;
459 continue;
462 if (!$this->getCurrentSubDocument($document, $key, $value, $current[$key])) {
463 throw new Exception("{$key}: Array and documents are not compatible");
465 } else if(!array_key_exists($key, $current) || $value !== $current[$key]) {
467 * It is 'linear' field that has changed, or
468 * has been modified.
470 $past_value = isset($current[$key]) ? $current[$key] : NULL;
471 $this->runFilter($key, $value, $past_value);
472 $document['$set'][$key] = $value;
474 } else {
476 * It is a document insertation, so we
477 * create the document.
479 $this->runFilter($key, $value, NULL);
480 $document[$key] = $value;
484 /* Updated behaves in a diff. way */
485 if ($update) {
486 foreach (array_diff(array_keys($this->_current), array_keys($object)) as $property) {
487 if ($property == '_id') {
488 continue;
490 $document['$unset'][$property] = 1;
494 if (count($document) == 0) {
495 return array();
498 $this->triggerEvent('after_validate', array(&$document));
499 $this->triggerEvent('after_validate_'.($update?'update':'creation'), array(&$object));
501 return $document;
503 // }}}
505 // }}}
507 // EVENT HANDLERS {{{
509 // addEvent($action, $callback) {{{
511 * addEvent
514 final static function addEvent($action, $callback)
516 if (!is_callable($callback)) {
517 throw new Exception("Invalid callback");
520 $class = get_called_class();
521 if ($class == __CLASS__) {
522 $events = & self::$_super_events;
523 } else {
524 $events = & self::$_events[$class];
526 if (!isset($events[$action])) {
527 $events[$action] = array();
529 $events[$action][] = $callback;
530 return TRUE;
532 // }}}
534 // triggerEvent(string $event, Array $events_params) {{{
535 final function triggerEvent($event, Array $events_params = array())
537 if (!isset($this)) {
538 $class = get_called_class();
539 } else {
540 $class = get_class($this);
542 $events = & self::$_events[$class][$event];
543 $sevents = & self::$_super_events[$event];
545 /* Super-Events handler receives the ActiveMongo class name as first param */
546 $sevents_params = array_merge(array($class), $events_params);
548 foreach (array('events', 'sevents') as $event_type) {
549 if (count($$event_type) > 0) {
550 $params = "{$event_type}_params";
551 foreach ($$event_type as $fnc) {
552 if (call_user_func_array($fnc, $$params) === FALSE) {
553 return;
559 /* Some natives events are allowed to be called
560 * as methods, if they exists
562 if (!isset($this)) {
563 return;
565 switch ($event) {
566 case 'before_create':
567 case 'before_update':
568 case 'before_validate':
569 case 'before_delete':
570 case 'before_drop':
571 case 'before_query':
572 case 'after_create':
573 case 'after_update':
574 case 'after_validate':
575 case 'after_delete':
576 case 'after_drop':
577 case 'after_query':
578 $fnc = array($this, $event);
579 $params = "events_params";
580 if (is_callable($fnc)) {
581 call_user_func_array($fnc, $$params);
583 break;
586 // }}}
588 // void runFilter(string $key, mixed &$value, mixed $past_value) {{{
590 * *Internal Method*
592 * This method check if the current document property has
593 * a filter method, if so, call it.
595 * If the filter returns FALSE, throw an Exception.
597 * @return void
599 protected function runFilter($key, &$value, $past_value)
601 $filter = array($this, "{$key}_filter");
602 if (is_callable($filter)) {
603 $filter = call_user_func_array($filter, array(&$value, $past_value));
604 if ($filter===FALSE) {
605 throw new ActiveMongo_FilterException("{$key} filter failed");
607 $this->$key = $value;
610 // }}}
612 // }}}
614 // void setCursor(MongoCursor $obj) {{{
616 * Set Cursor
618 * This method receive a MongoCursor and make
619 * it iterable.
621 * @param MongoCursor $obj
623 * @return void
625 final protected function setCursor(MongoCursor $obj)
627 $this->_cursor = $obj;
628 $obj->reset();
629 $this->setResult($obj->getNext());
631 // }}}
633 // void setResult(Array $obj) {{{
635 * Set Result
637 * This method takes an document and copy it
638 * as properties in this object.
640 * @param Array $obj
642 * @return void
644 final protected function setResult($obj)
646 /* Unsetting previous results, if any */
647 foreach (array_keys(get_document_vars($this, FALSE)) as $key) {
648 unset($this->$key);
650 $this->_id = NULL;
652 /* Add our current resultset as our object's property */
653 foreach ((array)$obj as $key => $value) {
654 if ($key[0] == '$') {
655 continue;
657 $this->$key = $value;
660 /* Save our record */
661 $this->_current = $obj;
663 // }}}
665 // this find([$_id]) {{{
667 * Simple find.
669 * Really simple find, which uses this object properties
670 * for fast filtering
672 * @return object this
674 final function find($_id = NULL)
676 $vars = get_document_vars($this);
677 $parent_class = __CLASS__;
678 foreach ($vars as $key => $value) {
679 if (!$value) {
680 unset($vars[$key]);
682 if ($value InstanceOf $parent_class) {
683 $this->getColumnDeference($vars, $key, $value);
684 unset($vars[$key]); /* delete old value */
687 if ($_id != NULL) {
688 if (is_array($_id)) {
689 $vars['_id'] = array('$in' => $_id);
690 } else {
691 $vars['_id'] = $_id;
694 $res = $this->_getCollection()->find($vars);
695 $this->setCursor($res);
696 return $this;
698 // }}}
700 // void save(bool $async) {{{
702 * Save
704 * This method save the current document in MongoDB. If
705 * we're modifying a document, a update is performed, otherwise
706 * the document is inserted.
708 * On updates, special operations such as $set, $pushAll, $pullAll
709 * and $unset in order to perform efficient updates
711 * @param bool $async
713 * @return void
715 final function save($async=TRUE)
717 $update = isset($this->_id) && $this->_id InstanceOf MongoID;
718 $conn = $this->_getCollection();
719 $document = $this->getCurrentDocument($update);
720 $object = $this->getDocumentVars();
722 if (isset($this->_id)) {
723 $object['_id'] = $this->_id;
726 if (count($document) == 0) {
727 return; /*nothing to do */
730 /* PRE-save hook */
731 $this->triggerEvent('before_'.($update ? 'update' : 'create'), array(&$document, $object));
733 if ($update) {
734 $conn->update(array('_id' => $this->_id), $document, array('safe' => $async));
735 if (isset($document['$set'])) {
736 foreach ($document['$set'] as $key => $value) {
737 if (strpos($key, ".") === FALSE) {
738 $this->_current[$key] = $value;
739 $this->$key = $value;
740 } else {
741 $keys = explode(".", $key);
742 $key = $keys[0];
743 $arr = & $this->$key;
744 $arrc = & $this->_current[$key];
745 for ($i=1; $i < count($keys)-1; $i++) {
746 $arr = &$arr[$keys[$i]];
747 $arrc = &$arrc[$keys[$i]];
749 $arr [ $keys[$i] ] = $value;
750 $arrc[ $keys[$i] ] = $value;
754 if (isset($document['$unset'])) {
755 foreach ($document['$unset'] as $key => $value) {
756 if (strpos($key, ".") === FALSE) {
757 unset($this->_current[$key]);
758 unset($this->$key);
759 } else {
760 $keys = explode(".", $key);
761 $key = $keys[0];
762 $arr = & $this->$key;
763 $arrc = & $this->_current[$key];
764 for ($i=1; $i < count($keys)-1; $i++) {
765 $arr = &$arr[$keys[$i]];
766 $arrc = &$arrc[$keys[$i]];
768 unset($arr [ $keys[$i] ]);
769 unset($arrc[ $keys[$i] ]);
773 } else {
774 $conn->insert($document, $async);
775 $this->setResult($document);
778 $this->triggerEvent('after_'.($update ? 'update' : 'create'), array($document, $object));
780 return TRUE;
782 // }}}
784 // bool delete() {{{
786 * Delete the current document
788 * @return bool
790 final function delete()
793 $document = array('_id' => $this->_id);
794 if ($this->_cursor InstanceOf MongoCursor) {
795 $this->triggerEvent('before_delete', array($document));
796 $result = $this->_getCollection()->remove($document);
797 $this->triggerEvent('after_delete', array($document));
798 $this->setResult(array());
799 return $result;
800 } else {
801 $criteria = (array) $this->_query;
803 /* remove */
804 $this->triggerEvent('before_delete', array($document));
805 $this->_getCollection()->remove($criteria);
806 $this->triggerEvent('after_delete', array($document));
808 /* reset object */
809 $this->reset();
811 return TRUE;
813 return FALSE;
815 // }}}
817 // Update {{{
819 * Multiple updates.
821 * This method perform multiple updates when a given
822 * criteria matchs (using where).
824 * By default the update is perform safely, but it can be
825 * changed.
827 * After the operation is done, the criteria is deleted.
829 * @param array $value Values to set
830 * @param bool $safe Whether or not peform the operation safely
832 * @return bool
835 function update(Array $value, $safe=TRUE)
837 $this->_assertNotInQuery();
839 $criteria = (array) $this->_query;
840 $options = array('multiple' => TRUE, 'safe' => $safe);
842 /* update */
843 $col = $this->_getCollection();
844 $col->update($criteria, array('$set' => $value), $options);
846 /* reset object */
847 $this->reset();
849 return TRUE;
851 // }}}
853 // void drop() {{{
855 * Delete the current colleciton and all its documents
857 * @return void
859 final static function drop()
861 $class = get_called_class();
862 if ($class == __CLASS__) {
863 return FALSE;
865 $obj = new $class;
866 $obj->triggerEvent('before_drop');
867 $result = $obj->_getCollection()->drop();
868 $obj->triggerEvent('after_drop');
869 if ($result['ok'] != 1) {
870 throw new ActiveMongo_Exception($result['errmsg']);
872 return TRUE;
875 // }}}
877 // int count() {{{
879 * Return the number of documents in the actual request. If
880 * we're not in a request, it will return 0.
882 * @return int
884 final function count()
886 if ($this->valid()) {
887 return $this->_cursor->count();
889 return 0;
891 // }}}
893 // void setup() {{{
895 * This method should contain all the indexes, and shard keys
896 * needed by the current collection. This try to make
897 * installation on development environments easier.
899 function setup()
902 // }}}
904 // batchInsert {{{
905 /**
906 * Perform a batchInsert of objects.
908 * @param array $documents Arrays of documents to insert
909 * @param bool $safe True if a safe will be performed, this means data validation, and wait for MongoDB OK reply
910 * @param bool $on_error_continue If an error happen while validating an object, if it should continue or not
912 * @return bool
914 final public static function batchInsert(Array $documents, $safe=TRUE, $on_error_continue=TRUE)
916 if (__CLASS__ == get_called_class()) {
917 throw new ActiveMongo_Exception("Invalid batchInsert usage");
920 if ($safe) {
921 foreach ($documents as $id => $doc) {
922 $valid = FALSE;
923 if (is_array($doc)) {
924 try {
925 self::triggerEvent('before_create', array(&$doc));
926 self::triggerEvent('before_validate', array(&$doc, $doc));
927 self::triggerEvent('before_validate_creation', array(&$doc, $doc));
928 $valid = TRUE;
929 } catch (Exception $e) {}
931 if (!$valid) {
932 if (!$on_error_continue) {
933 throw new ActiveMongo_FilterException("Document $id is invalid");
935 unset($documents[$id]);
940 return self::_getCollection()->batchInsert($documents, array("safe" => $safe));
942 // }}}
944 // bool addIndex(array $columns, array $options) {{{
946 * addIndex
948 * Create an Index in the current collection.
950 * @param array $columns L ist of columns
951 * @param array $options Options
953 * @return bool
955 final function addIndex($columns, $options=array())
957 $default_options = array(
958 'background' => 1,
961 foreach ($default_options as $option => $value) {
962 if (!isset($options[$option])) {
963 $options[$option] = $value;
967 $collection = $this->_getCollection();
969 return $collection->ensureIndex($columns, $options);
971 // }}}
973 // Array getIndexes() {{{
975 * Return an array with all indexes
977 * @return array
979 final static function getIndexes()
981 return self::_getCollection()->getIndexInfo();
983 // }}}
985 // string __toString() {{{
987 * To String
989 * If this object is treated as a string,
990 * it would return its ID.
992 * @return string
994 function __toString()
996 return (string)$this->getID();
998 // }}}
1000 // array sendCmd(array $cmd) {{{
1002 * This method sends a command to the current
1003 * database.
1005 * @param array $cmd Current command
1007 * @return array
1009 final protected function sendCmd($cmd)
1011 return $this->_getConnection()->command($cmd);
1013 // }}}
1015 // ITERATOR {{{
1017 // array getArray() {{{
1019 * Return the current document as an array
1020 * instead of a ActiveMongo object
1022 * @return Array
1024 final function getArray()
1026 return get_document_vars($this);
1028 // }}}
1030 // void reset() {{{
1032 * Reset our Object, delete the current cursor if any, and reset
1033 * unsets the values.
1035 * @return void
1037 final function reset()
1039 $this->_properties = NULL;
1040 $this->_cursor = NULL;
1041 $this->_cursor_ex = NULL;
1042 $this->_query = NULL;
1043 $this->_sort = NULL;
1044 $this->_limit = 0;
1045 $this->_skip = 0;
1046 $this->setResult(array());
1048 // }}}
1050 // bool valid() {{{
1052 * Valid
1054 * Return if we're on an iteration and if it is still valid
1056 * @return TRUE
1058 final function valid()
1060 $valid = FALSE;
1061 if (!$this->_cursor_ex) {
1062 if (!$this->_cursor InstanceOf MongoCursor) {
1063 $this->doQuery();
1065 $valid = $this->_cursor InstanceOf MongoCursor && $this->_cursor->valid();
1066 } else {
1067 switch ($this->_cursor_ex) {
1068 case self::FIND_AND_MODIFY:
1069 if ($this->_limit > $this->_findandmodify_cnt) {
1070 $this->_execFindAndModify();
1071 $valid = $this->_cursor_ex_value['ok'] == 1;
1073 break;
1074 default:
1075 throw new ActiveMongo_Exception("Invalid _cursor_ex value");
1079 return $valid;
1081 // }}}
1083 // bool next() {{{
1085 * Move to the next document
1087 * @return bool
1089 final function next()
1091 if ($this->_cloned) {
1092 throw new ActiveMongo_Exception("Cloned objects can't iterate");
1094 if (!$this->_cursor_ex) {
1095 $result = $this->_cursor->next();
1096 $this->current();
1097 return $result;
1098 } else {
1099 switch ($this->_cursor_ex) {
1100 case self::FIND_AND_MODIFY:
1101 $this->_cursor_ex_value = NULL;
1102 break;
1103 default:
1104 throw new ActiveMongo_Exception("Invalid _cursor_ex value");
1108 // }}}
1110 // this current() {{{
1112 * Return the current object, and load the current document
1113 * as this object property
1115 * @return object
1117 final function current()
1119 if (!$this->_cursor_ex) {
1120 $this->setResult($this->_cursor->current());
1121 } else {
1122 switch ($this->_cursor_ex) {
1123 case self::FIND_AND_MODIFY:
1124 if (count($this->_cursor_ex_value) == 0) {
1125 $this->_execFindAndModify();
1127 $this->setResult($this->_cursor_ex_value['value']);
1128 break;
1129 default:
1130 throw new ActiveMongo_Exception("Invalid _cursor_ex value");
1133 return $this;
1135 // }}}
1137 // bool rewind() {{{
1139 * Go to the first document
1141 final function rewind()
1143 if (!$this->_cursor_ex) {
1144 /* rely on MongoDB cursor */
1145 if (!$this->_cursor InstanceOf MongoCursor) {
1146 $this->doQuery();
1148 $result = $this->_cursor->rewind();
1149 $this->current();
1150 return $result;
1151 } else {
1152 switch ($this->_cursor_ex) {
1153 case self::FIND_AND_MODIFY:
1154 $this->_findandmodify_cnt = 0;
1155 break;
1156 default:
1157 throw new ActiveMongo_Exception("Invalid _cursor_ex value");
1161 // }}}
1163 // }}}
1165 // ARRAY ACCESS {{{
1166 final function offsetExists($offset)
1168 return isset($this->$offset);
1171 final function offsetGet($offset)
1173 return $this->$offset;
1176 final function offsetSet($offset, $value)
1178 $this->$offset = $value;
1181 final function offsetUnset($offset)
1183 unset($this->$offset);
1185 // }}}
1187 // REFERENCES {{{
1189 // array getReference() {{{
1191 * ActiveMongo extended the Mongo references, adding
1192 * the concept of 'dynamic' requests, saving in the database
1193 * the current query with its options (sort, limit, etc).
1195 * This is useful to associate a document with a given
1196 * request. To undestand this better please see the 'reference'
1197 * example.
1199 * @return array
1201 final function getReference($dynamic=FALSE)
1203 if (!$this->getID() && !$dynamic) {
1204 return NULL;
1207 $document = array(
1208 '$ref' => $this->getCollectionName(),
1209 '$id' => $this->getID(),
1210 '$db' => $this->getDatabaseName(),
1211 'class' => get_class($this),
1214 if ($dynamic) {
1215 if (!$this->_cursor InstanceOf MongoCursor && $this->_cursor_ex === NULL) {
1216 $this->doQuery();
1219 if (!$this->_cursor InstanceOf MongoCursor) {
1220 throw new ActiveMongo_Exception("Only MongoDB native cursor could have dynamic references");
1223 $cursor = $this->_cursor;
1224 if (!is_callable(array($cursor, "Info"))) {
1225 throw new Exception("Please upgrade your PECL/Mongo module to use this feature");
1227 $document['dynamic'] = array();
1228 $query = $cursor->Info();
1229 foreach ($query as $type => $value) {
1230 $document['dynamic'][$type] = $value;
1233 return $document;
1235 // }}}
1237 // void getDocumentReferences($document, &$refs) {{{
1239 * Get Current References
1241 * Inspect the current document trying to get any references,
1242 * if any.
1244 * @param array $document Current document
1245 * @param array &$refs References found in the document.
1246 * @param array $parent_key Parent key
1248 * @return void
1250 final protected function getDocumentReferences($document, &$refs, $parent_key=NULL)
1252 foreach ($document as $key => $value) {
1253 if (is_array($value)) {
1254 if (MongoDBRef::isRef($value)) {
1255 $pkey = $parent_key;
1256 $pkey[] = $key;
1257 $refs[] = array('ref' => $value, 'key' => $pkey);
1258 } else {
1259 $parent_key1 = $parent_key;
1260 $parent_key1[] = $key;
1261 $this->getDocumentReferences($value, $refs, $parent_key1);
1266 // }}}
1268 // object _deferencingCreateObject(string $class) {{{
1270 * Called at deferencig time
1272 * Check if the given string is a class, and it is a sub class
1273 * of ActiveMongo, if it is instance and return the object.
1275 * @param string $class
1277 * @return object
1279 private function _deferencingCreateObject($class)
1281 if (!is_subclass_of($class, __CLASS__)) {
1282 throw new ActiveMongo_Exception("Fatal Error, imposible to create ActiveMongo object of {$class}");
1284 return new $class;
1286 // }}}
1288 // void _deferencingRestoreProperty(array &$document, array $keys, mixed $req) {{{
1290 * Called at deferencig time
1292 * This method iterates $document until it could match $keys path, and
1293 * replace its value by $req.
1295 * @param array &$document Document to replace
1296 * @param array $keys Path of property to change
1297 * @param mixed $req Value to replace.
1299 * @return void
1301 private function _deferencingRestoreProperty(&$document, $keys, $req)
1303 $obj = & $document;
1305 /* find the $req proper spot */
1306 foreach ($keys as $key) {
1307 $obj = & $obj[$key];
1310 $obj = $req;
1312 /* Delete reference variable */
1313 unset($obj);
1315 // }}}
1317 // object _deferencingQuery($request) {{{
1319 * Called at deferencig time
1321 * This method takes a dynamic reference and request
1322 * it to MongoDB.
1324 * @param array $request Dynamic reference
1326 * @return this
1328 private function _deferencingQuery($request)
1330 $collection = $this->_getCollection();
1331 $cursor = $collection->find($request['query'], $request['fields']);
1332 if ($request['limit'] > 0) {
1333 $cursor->limit($request['limit']);
1335 if ($request['skip'] > 0) {
1336 $cursor->skip($request['skip']);
1339 $this->setCursor($cursor);
1341 return $this;
1343 // }}}
1345 // void doDeferencing() {{{
1347 * Perform a deferencing in the current document, if there is
1348 * any reference.
1350 * ActiveMongo will do its best to group references queries as much
1351 * as possible, in order to perform as less request as possible.
1353 * ActiveMongo doesn't rely on MongoDB references, but it can support
1354 * it, but it is prefered to use our referencing.
1356 * @experimental
1358 final function doDeferencing($refs=array())
1360 /* Get current document */
1361 $document = get_document_vars($this);
1363 if (count($refs)==0) {
1364 /* Inspect the whole document */
1365 $this->getDocumentReferences($document, $refs);
1368 $db = $this->_getConnection();
1370 /* Gather information about ActiveMongo Objects
1371 * that we need to create
1373 $classes = array();
1374 foreach ($refs as $ref) {
1375 if (!isset($ref['ref']['class'])) {
1377 /* Support MongoDBRef, we do our best to be compatible {{{ */
1378 /* MongoDB 'normal' reference */
1380 $obj = MongoDBRef::get($db, $ref['ref']);
1382 /* Offset the current document to the right spot */
1383 /* Very inefficient, never use it, instead use ActiveMongo References */
1385 $this->_deferencingRestoreProperty($document, $ref['key'], $obj);
1387 /* Dirty hack, override our current document
1388 * property with the value itself, in order to
1389 * avoid replace a MongoDB reference by its content
1391 $this->_deferencingRestoreProperty($this->_current, $ref['key'], $obj);
1393 /* }}} */
1395 } else {
1397 if (isset($ref['ref']['dynamic'])) {
1398 /* ActiveMongo Dynamic Reference */
1400 /* Create ActiveMongo object */
1401 $req = $this->_deferencingCreateObject($ref['ref']['class']);
1403 /* Restore saved query */
1404 $req->_deferencingQuery($ref['ref']['dynamic']);
1406 $results = array();
1408 /* Add the result set */
1409 foreach ($req as $result) {
1410 $results[] = clone $result;
1413 /* add information about the current reference */
1414 foreach ($ref['ref'] as $key => $value) {
1415 $results[$key] = $value;
1418 $this->_deferencingRestoreProperty($document, $ref['key'], $results);
1420 } else {
1421 /* ActiveMongo Reference FTW! */
1422 $classes[$ref['ref']['class']][] = $ref;
1427 /* {{{ Create needed objects to query MongoDB and replace
1428 * our references by its objects documents.
1430 foreach ($classes as $class => $refs) {
1431 $req = $this->_deferencingCreateObject($class);
1433 /* Load list of IDs */
1434 $ids = array();
1435 foreach ($refs as $ref) {
1436 $ids[] = $ref['ref']['$id'];
1439 /* Search to MongoDB once for all IDs found */
1440 $req->find($ids);
1443 /* Replace our references by its objects */
1444 foreach ($refs as $ref) {
1445 $id = $ref['ref']['$id'];
1446 $place = $ref['key'];
1447 $req->rewind();
1448 while ($req->getID() != $id && $req->next());
1450 $this->_deferencingRestoreProperty($document, $place, clone $req);
1452 unset($obj);
1455 /* Release request, remember we
1456 * safely cloned it,
1458 unset($req);
1460 // }}}
1462 /* Replace the current document by the new deferenced objects */
1463 foreach ($document as $key => $value) {
1464 $this->$key = $value;
1467 // }}}
1469 // void getColumnDeference(&$document, $propety, ActiveMongo Obj) {{{
1471 * Prepare a "selector" document to search treaing the property
1472 * as a reference to the given ActiveMongo object.
1475 final function getColumnDeference(&$document, $property, ActiveMongo $obj)
1477 $document["{$property}.\$id"] = $obj->getID();
1479 // }}}
1481 // void findReferences(&$document) {{{
1483 * Check if in the current document to insert or update
1484 * exists any references to other ActiveMongo Objects.
1486 * @return void
1488 final function findReferences(&$document)
1490 if (!is_array($document)) {
1491 return;
1493 foreach($document as &$value) {
1494 $parent_class = __CLASS__;
1495 if (is_array($value)) {
1496 if (MongoDBRef::isRef($value)) {
1497 /* If the property we're inspecting is a reference,
1498 * we need to remove the values, restoring the valid
1499 * Reference.
1501 $arr = array(
1502 '$ref'=>1, '$id'=>1, '$db'=>1, 'class'=>1, 'dynamic'=>1
1504 foreach (array_keys($value) as $key) {
1505 if (!isset($arr[$key])) {
1506 unset($value[$key]);
1509 } else {
1510 $this->findReferences($value);
1512 } else if ($value InstanceOf $parent_class) {
1513 $value = $value->getReference();
1516 /* trick: delete last var. reference */
1517 unset($value);
1519 // }}}
1521 // void __clone() {{{
1522 /**
1523 * Cloned objects are rarely used, but ActiveMongo
1524 * uses it to create different objects per everyrecord,
1525 * which is used at deferencing. Therefore cloned object
1526 * do not contains the recordset, just the actual document,
1527 * so iterations are not allowed.
1530 final function __clone()
1532 unset($this->_cursor);
1533 $this->_cloned = TRUE;
1535 // }}}
1537 // }}}
1539 // GET DOCUMENT ID {{{
1541 // getID() {{{
1543 * Return the current document ID. If there is
1544 * no document it would return FALSE.
1546 * @return object|FALSE
1548 final public function getID()
1550 if ($this->_id instanceof MongoID) {
1551 return $this->_id;
1553 return FALSE;
1555 // }}}
1557 // string key() {{{
1559 * Return the current key
1561 * @return string
1563 final function key()
1565 return (string)$this->getID();
1567 // }}}
1569 // }}}
1571 // Fancy (and silly) query abstraction {{{
1573 // _assertNotInQuery() {{{
1575 * Check if we can modify the query or not. We cannot modify
1576 * the query if we're iterating over and oldest query, in this case the
1577 * object must be reset.
1579 * @return void
1581 final private function _assertNotInQuery()
1583 if ($this->_cursor InstanceOf MongoCursor || $this->_cursor_ex != NULL) {
1584 throw new ActiveMongo_Exception("You cannot modify the query, please reset the object");
1587 // }}}
1589 // bool servedFromCache() {{{
1591 * Return True if the current result
1592 * was provided by a before_query hook (aka cache)
1593 * or False if it was retrieved from MongoDB
1595 * @return bool
1597 final function servedFromCache()
1599 return $this->_cached;
1601 // }}}
1603 // doQuery() {{{
1605 * Build the current request and send it to MongoDB.
1607 * @return this
1609 final function doQuery($use_cache=TRUE)
1611 if ($this->_cursor_ex) {
1612 switch ($this->_cursor_ex) {
1613 case self::FIND_AND_MODIFY:
1614 $this->_cursor_ex_value = NULL;
1615 return;
1616 default:
1617 throw new ActiveMongo_Exception("Invalid _cursor_ex value");
1620 $this->_assertNotInQuery();
1622 $query = array(
1623 'collection' => $this->getCollectionName(),
1624 'query' => (array)$this->_query,
1625 'properties' => (array)$this->_properties,
1626 'sort' => (array)$this->_sort,
1627 'skip' => $this->_skip,
1628 'limit' => $this->_limit
1631 $this->_cached = FALSE;
1633 self::triggerEvent('before_query', array(&$query, &$documents, $use_cache));
1635 if ($documents InstanceOf MongoCursor && $use_cache) {
1636 $this->_cached = TRUE;
1637 $this->setCursor($documents);
1638 return $this;
1641 $col = $this->_getCollection();
1642 if (count($query['properties']) > 0) {
1643 $cursor = $col->find($query['query'], $query['properties']);
1644 } else {
1645 $cursor = $col->find($query['query']);
1647 if (count($query['sort']) > 0) {
1648 $cursor->sort($query['sort']);
1650 if ($query['limit'] > 0) {
1651 $cursor->limit($query['limit']);
1653 if ($query['skip'] > 0) {
1654 $cursor->skip($query['skip']);
1657 self::triggerEvent('after_query', array($query, $cursor));
1659 /* Our cursor must be sent to ActiveMongo */
1660 $this->setCursor($cursor);
1662 return $this;
1664 // }}}
1666 // properties($props) {{{
1668 * Select 'properties' or 'columns' to be included in the document,
1669 * by default all properties are included.
1671 * @param array $props
1673 * @return this
1675 final function properties($props)
1677 $this->_assertNotInQuery();
1679 if (!is_array($props) && !is_string($props)) {
1680 return FALSE;
1683 if (is_string($props)) {
1684 $props = explode(",", $props);
1687 foreach ($props as $id => $name) {
1688 $props[trim($name)] = 1;
1689 unset($props[$id]);
1693 /* _id should always be included */
1694 $props['_id'] = 1;
1696 $this->_properties = $props;
1698 return $this;
1701 final function columns($properties)
1703 return $this->properties($properties);
1705 // }}}
1707 // where($property, $value) {{{
1709 * Where abstraction.
1712 final function where($property_str, $value=NULL)
1714 $this->_assertNotInQuery();
1716 if (is_array($property_str)) {
1717 if ($value != NULL) {
1718 throw new ActiveMongo_Exception("Invalid parameters");
1720 foreach ($property_str as $property => $value) {
1721 if (is_numeric($property)) {
1722 $property = $value;
1723 $value = NULL;
1725 $this->where($property, $value);
1727 return $this;
1730 $column = explode(" ", trim($property_str));
1731 if (count($column) != 1 && count($column) != 2) {
1732 throw new ActiveMongo_Exception("Failed while parsing '{$property_str}'");
1733 } else if (count($column) == 2) {
1735 $exp_scalar = TRUE;
1736 switch (strtolower($column[1])) {
1737 case '>':
1738 case '$gt':
1739 $op = '$gt';
1740 break;
1742 case '>=':
1743 case '$gte':
1744 $op = '$gte';
1745 break;
1747 case '<':
1748 case '$lt':
1749 $op = '$lt';
1750 break;
1752 case '<=':
1753 case '$lte':
1754 $op = '$lte';
1755 break;
1757 case '==':
1758 case '$eq':
1759 case '=':
1760 if (is_array($value)) {
1761 $op = '$all';
1762 $exp_scalar = FALSE;
1763 } else {
1764 $op = NULL;
1766 break;
1768 case '!=':
1769 case '<>':
1770 case '$ne':
1771 if (is_array($value)) {
1772 $op = '$nin';
1773 $exp_scalar = FALSE;
1774 } else {
1775 $op = '$ne';
1777 break;
1779 case '%':
1780 case 'mod':
1781 case '$mod':
1782 $op = '$mod';
1783 $exp_scalar = FALSE;
1784 break;
1786 case 'exists':
1787 case '$exists':
1788 if ($value === NULL) {
1789 $value = 1;
1791 $op = '$exists';
1792 break;
1794 /* regexp */
1795 case 'regexp':
1796 case 'regex':
1797 $value = new MongoRegex($value);
1798 $op = NULL;
1799 break;
1801 /* arrays */
1802 case 'in':
1803 case '$in':
1804 $exp_scalar = FALSE;
1805 $op = '$in';
1806 break;
1808 case '$nin':
1809 case 'nin':
1810 $exp_scalar = FALSE;
1811 $op = '$nin';
1812 break;
1815 /* geo operations */
1816 case 'near':
1817 case '$near':
1818 $op = '$near';
1819 $exp_scalar = FALSE;
1820 break;
1822 default:
1823 throw new ActiveMongo_Exception("Failed to parse '{$column[1]}'");
1826 if ($exp_scalar && is_array($value)) {
1827 throw new ActiveMongo_Exception("Cannot use comparing operations with Array");
1828 } else if (!$exp_scalar && !is_array($value)) {
1829 throw new ActiveMongo_Exception("The operation {$column[1]} expected an Array");
1832 if ($op) {
1833 $value = array($op => $value);
1835 } else if (is_array($value)) {
1836 $value = array('$in' => $value);
1839 $spot = & $this->_query[$column[0]];
1840 if (is_array($spot) && is_array($value)) {
1841 $spot[key($value)] = current($value);
1842 } else {
1843 /* simulate AND among same properties if
1844 * multiple values is passed for same property
1846 if (isset($spot)) {
1847 if (is_array($spot)) {
1848 $spot['$all'][] = $value;
1849 } else {
1850 $spot = array('$all' => array($spot, $value));
1852 } else {
1853 $spot = $value;
1857 return $this;
1859 // }}}
1861 // sort($sort_str) {{{
1863 * Abstract the documents sorting.
1865 * @param string $sort_str List of properties to use as sorting
1867 * @return this
1869 final function sort($sort_str)
1871 $this->_assertNotInQuery();
1873 $this->_sort = array();
1874 foreach ((array)explode(",", $sort_str) as $sort_part_str) {
1875 $sort_part = explode(" ", trim($sort_part_str), 2);
1876 switch(count($sort_part)) {
1877 case 1:
1878 $sort_part[1] = 'ASC';
1879 break;
1880 case 2:
1881 break;
1882 default:
1883 throw new ActiveMongo_Exception("Don't know how to parse {$sort_part_str}");
1886 switch (strtoupper($sort_part[1])) {
1887 case 'ASC':
1888 $sort_part[1] = 1;
1889 break;
1890 case 'DESC':
1891 $sort_part[1] = -1;
1892 break;
1893 default:
1894 throw new ActiveMongo_Exception("Invalid sorting direction `{$sort_part[1]}`");
1896 $this->_sort[ $sort_part[0] ] = $sort_part[1];
1899 return $this;
1901 // }}}
1903 // limit($limit, $skip) {{{
1905 * Abstract the limitation and pagination of documents.
1907 * @param int $limit Number of max. documents to retrieve
1908 * @param int $skip Number of documents to skip
1910 * @return this
1912 final function limit($limit=0, $skip=0)
1914 $this->_assertNotInQuery();
1916 if ($limit < 0 || $skip < 0) {
1917 return FALSE;
1919 $this->_limit = $limit;
1920 $this->_skip = $skip;
1922 return $this;
1924 // }}}
1926 // FindAndModify(Array $document) {{{
1928 * findAndModify
1932 final function findAndModify($document)
1934 $this->_assertNotInQuery();
1936 if (count($document) === 0) {
1937 throw new ActiveMongo_Exception("Empty \$document is not allowed");
1940 $this->_cursor_ex = self::FIND_AND_MODIFY;
1941 $this->_findandmodify = $document;
1943 return $this;
1946 private function _execFindAndModify()
1948 $query = (array)$this->_query;
1950 $query = array(
1951 "findandmodify" => $this->getCollectionName(),
1952 "query" => $query,
1953 "update" => array('$set' => $this->_findandmodify),
1954 "new" => TRUE,
1956 if (isset($this->_sort)) {
1957 $query["sort"] = $this->_sort;
1959 $this->_cursor_ex_value = $this->sendCMD($query);
1961 $this->_findandmodify_cnt++;
1963 // }}}
1965 // }}}
1967 // __sleep() {{{
1969 * Return a list of properties to serialize, to save
1970 * into MongoDB
1972 * @return array
1974 function __sleep()
1976 return array_keys(get_document_vars($this));
1978 // }}}
1982 require_once dirname(__FILE__)."/Validators.php";
1983 require_once dirname(__FILE__)."/Exceptions.php";
1986 * Local variables:
1987 * tab-width: 4
1988 * c-basic-offset: 4
1989 * End:
1990 * vim600: sw=4 ts=4 fdm=marker
1991 * vim<600: sw=4 ts=4