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. |
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. |
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. |
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. |
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) {{{
40 * Simple hack to avoid get private and protected variables
43 * @param bool $include_id
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();
57 if (version_compare(PHP_VERSION
, '5.3') < 0) {
58 require dirname(__FILE__
)."/Objects_compat.php";
60 require dirname(__FILE__
)."/Objects.php";
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
76 abstract class ActiveMongo
implements Iterator
, Countable
, ArrayAccess
80 const FIND_AND_MODIFY
= 0x001;
85 * Current databases objects
95 private static $_namespace = NULL;
97 * Specific namespaces for each class
101 private static $_namespaces = array();
103 * Current collections objects
107 private static $_collections;
109 * Current connection to MongoDB
111 * @type MongoConnection
113 private static $_conn;
121 * List of events handlers
125 static private $_events = array();
127 * List of global events handlers
131 static private $_super_events = array();
137 private static $_host;
143 private static $_user;
150 private static $_pwd;
157 private $_current = array();
163 private $_cursor = NULL;
165 * Extended result cursor, used for FindAndModify now
169 private $_cursor_ex = NULL;
170 private $_cursor_ex_value;
172 * Count the findandmodify result counts
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;
186 private $_properties = NULL;
190 * Current document ID
197 * Tell if the current object
202 private $_cloned = FALSE;
205 // GET CONNECTION CONFIG {{{
207 // setNameSpace($namespace='') {{{
209 * Set a namespace for all connections is it is called
210 * statically from ActiveMongo or for specific classes
211 * if it is called from an instance.
213 * @param string $namespace
217 final static function setNamespace($namespace='')
219 if (preg_match("#^[\-\_a-z0-9]*$#i", $namespace)) {
220 /* sort of standard late binding */
222 $context = get_class($this);
224 $context = get_called_class();
227 if ($context == __CLASS__
) {
228 self
::$_namespace = $namespace;
230 self
::$_namespaces[$context] = $namespace;
238 // collectionName() {{{
242 * Return the collection name (along with its namespace) for
243 * the current object.
245 * Warning: This must not be called statically from outside the
250 final public function collectionName()
253 /* Need to check if $this is instance of $parent
254 * because PHP5.2 fails detecting $this when a non-static
255 * method is called statically from another class ($this is
258 if (isset($this) && $this InstanceOf $parent) {
259 $collection = $this->getCollectionName();
260 $context = get_class($this);
262 /* ugly, it might fail if getCollectionName has some refernce to $this */
263 $context = get_called_class();
264 $collection = call_user_func(array($context, 'getCollectionName'));
267 if (isset(self
::$_namespaces[$context]) && self
::$_namespaces[$context]) {
268 $collection = self
::$_namespaces[$context].".{$collection}";
269 } else if (self
::$_namespace) {
270 $collection = self
::$_namespace.".{$collection}";
277 // string getCollectionName() {{{
279 * Get Collection Name, by default the class name,
280 * but you it can be override at the class itself to give
283 * @return string Collection Name
285 protected function getCollectionName()
288 return strtolower(get_class($this));
290 return strtolower(get_called_class());
295 // string getDatabaseName() {{{
297 * Get Database Name, by default it is used
298 * the db name set by ActiveMong::connect()
300 * @return string DB Name
302 protected function getDatabaseName()
304 if (is_NULL(self
::$_db)) {
305 throw new ActiveMongo_Exception("There is no information about the default DB name");
311 // void install() {{{
315 * This static method iterate over the classes lists,
316 * and execute the setup() method on every ActiveMongo
317 * subclass. You should do this just once.
320 final public static function install()
322 $classes = array_reverse(get_declared_classes());
323 foreach ($classes as $class)
325 if ($class == __CLASS__
) {
328 if (is_subclass_of($class, __CLASS__
)) {
336 // void connection($db, $host) {{{
340 * This method setup parameters to connect to a MongoDB
341 * database. The connection is done when it is needed.
343 * @param string $db Database name
344 * @param string $host Host to connect
345 * @param string $user User (Auth)
346 * @param string $pwd Password (Auth)
350 final public static function connect($db, $host='localhost', $user = NULL, $pwd=NULL)
352 self
::$_host = $host;
354 self
::$_user = $user;
359 // MongoConnection _getConnection() {{{
363 * Get a valid database connection
365 * @return MongoConnection
367 final protected function _getConnection()
369 if (is_NULL(self
::$_conn)) {
370 if (is_NULL(self
::$_host)) {
371 self
::$_host = 'localhost';
373 self
::$_conn = new Mongo(self
::$_host);
376 $dbname = $this->getDatabaseName();
378 $dbname = self
::getDatabaseName();
380 if (!isSet(self
::$_dbs[$dbname])) {
381 self
::$_dbs[$dbname] = self
::$_conn->selectDB($dbname);
383 if ( !is_NULL(self
::$_user ) && !is_NULL(self
::$_pwd ) ) {
384 self
::$_dbs[$dbname]->authenticate(self
::$_user,self
::$_pwd);
388 return self
::$_dbs[$dbname];
392 // MongoCollection _getCollection() {{{
396 * Get a collection connection.
398 * @return MongoCollection
400 final protected function _getCollection()
403 $colName = $this->CollectionName();
405 $colName = self
::CollectionName();
407 if (!isset(self
::$_collections[$colName])) {
408 self
::$_collections[$colName] = self
::_getConnection()->selectCollection($colName);
410 return self
::$_collections[$colName];
416 // GET DOCUMENT TO SAVE OR UPDATE {{{
418 // getDocumentVars() {{{
425 final protected function getDocumentVars()
427 $variables = array();
428 foreach ((array)$this->__sleep() as $var) {
429 if (!property_exists($this, $var)) {
432 $variables[$var] = $this->$var;
438 // bool getCurrentSubDocument(array &$document, string $parent_key, array $values, array $past_values) {{{
440 * Generate Sub-document
442 * This method build the difference between the current sub-document,
443 * and the origin one. If there is no difference, it would do nothing,
444 * otherwise it would build a document containing the differences.
446 * @param array &$document Document target
447 * @param string $parent_key Parent key name
448 * @param array $values Current values
449 * @param array $past_values Original values
453 final function getCurrentSubDocument(&$document, $parent_key, Array $values, Array $past_values)
456 * The current property is a embedded-document,
457 * now we're looking for differences with the
458 * previous value (because we're on an update).
460 * It behaves exactly as getCurrentDocument,
461 * but this is simples (it doesn't support
464 foreach ($values as $key => $value) {
465 $super_key = "{$parent_key}.{$key}";
466 if (is_array($value)) {
468 * Inner document detected
470 if (!array_key_exists($key, $past_values) ||
!is_array($past_values[$key])) {
472 * We're lucky, it is a new sub-document,
475 $document['$set'][$super_key] = $value;
478 * This is a document like this, we need
479 * to find out the differences to avoid
482 if (!$this->getCurrentSubDocument($document, $super_key, $value, $past_values[$key])) {
487 } else if (!array_key_exists($key, $past_values) ||
$past_values[$key] !== $value) {
488 $document['$set'][$super_key] = $value;
492 foreach (array_diff(array_keys($past_values), array_keys($values)) as $key) {
493 $super_key = "{$parent_key}.{$key}";
494 $document['$unset'][$super_key] = 1;
501 // array getCurrentDocument(bool $update) {{{
503 * Get Current Document
505 * Based on this object properties a new document (Array)
506 * is returned. If we're modifying an document, just the modified
507 * properties are included in this document, which uses $set,
508 * $unset, $pushAll and $pullAll.
511 * @param bool $update
515 final protected function getCurrentDocument($update=FALSE, $current=FALSE)
518 $object = $this->getDocumentVars();
521 $current = (array)$this->_current
;
525 $this->findReferences($object);
527 $this->triggerEvent('before_validate', array(&$object, $current));
528 $this->triggerEvent('before_validate_'.($update?
'update':'creation'), array(&$object, $current));
530 foreach ($object as $key => $value) {
532 if (is_array($value) && isset($current[$key])) {
534 * If the Field to update is an array, it has a different
535 * behaviour other than $set and $unset. Fist, we need
536 * need to check if it is an array or document, because
537 * they can't be mixed.
540 if (!is_array($current[$key])) {
542 * We're lucky, the field wasn't
543 * an array previously.
545 $this->runFilter($key, $value, $current[$key]);
546 $document['$set'][$key] = $value;
550 if (!$this->getCurrentSubDocument($document, $key, $value, $current[$key])) {
551 throw new Exception("{$key}: Array and documents are not compatible");
553 } else if(!array_key_exists($key, $current) ||
$value !== $current[$key]) {
555 * It is 'linear' field that has changed, or
558 $past_value = isset($current[$key]) ?
$current[$key] : NULL;
559 $this->runFilter($key, $value, $past_value);
560 $document['$set'][$key] = $value;
564 * It is a document insertation, so we
565 * create the document.
567 $this->runFilter($key, $value, NULL);
568 $document[$key] = $value;
572 /* Updated behaves in a diff. way */
574 foreach (array_diff(array_keys($this->_current
), array_keys($object)) as $property) {
575 if ($property == '_id') {
578 $document['$unset'][$property] = 1;
582 if (count($document) == 0) {
586 $this->triggerEvent('after_validate', array(&$document));
587 $this->triggerEvent('after_validate_'.($update?
'update':'creation'), array(&$object));
595 // EVENT HANDLERS {{{
597 // addEvent($action, $callback) {{{
602 final static function addEvent($action, $callback)
604 if (!is_callable($callback)) {
605 throw new ActiveMongo_Exception("Invalid callback");
608 $class = get_called_class();
609 if ($class == __CLASS__
) {
610 $events = & self
::$_super_events;
612 $events = & self
::$_events[$class];
614 if (!isset($events[$action])) {
615 $events[$action] = array();
617 $events[$action][] = $callback;
622 // triggerEvent(string $event, Array $events_params) {{{
623 final function triggerEvent($event, Array $events_params = array(), $context=NULL)
627 $class = get_called_class();
630 $class = get_class($this);
637 $events = & self
::$_events[$class][$event];
638 $sevents = & self
::$_super_events[$event];
640 /* Super-Events handler receives the ActiveMongo class name as first param */
641 $sevents_params = array_merge(array($class), $events_params);
643 foreach (array('events', 'sevents') as $event_type) {
644 if (count($
$event_type) > 0) {
645 $params = "{$event_type}_params";
646 foreach ($
$event_type as $fnc) {
647 if (call_user_func_array($fnc, $
$params) === FALSE) {
655 case 'before_create':
656 case 'before_update':
657 case 'before_validate':
658 case 'before_delete':
663 case 'after_validate':
667 $fnc = array($obj, $event);
668 $params = "events_params";
669 if (is_callable($fnc)) {
670 call_user_func_array($fnc, $
$params);
677 // void runFilter(string $key, mixed &$value, mixed $past_value) {{{
681 * This method check if the current document property has
682 * a filter method, if so, call it.
684 * If the filter returns FALSE, throw an Exception.
688 protected function runFilter($key, &$value, $past_value)
690 $filter = array($this, "{$key}_filter");
691 if (is_callable($filter)) {
692 $filter = call_user_func_array($filter, array(&$value, $past_value));
693 if ($filter===FALSE) {
694 throw new ActiveMongo_FilterException("{$key} filter failed");
696 $this->$key = $value;
703 // void setCursor(MongoCursor $obj) {{{
707 * This method receive a MongoCursor and make
710 * @param MongoCursor $obj
714 final protected function setCursor(MongoCursor
$obj)
716 $this->_cursor
= $obj;
718 $this->setResult($obj->getNext());
722 // void setResult(Array $obj) {{{
726 * This method takes an document and copy it
727 * as properties in this object.
733 final protected function setResult($obj)
735 /* Unsetting previous results, if any */
736 foreach (array_keys(get_document_vars($this, FALSE)) as $key) {
741 /* Add our current resultset as our object's property */
742 foreach ((array)$obj as $key => $value) {
743 if ($key[0] == '$') {
746 $this->$key = $value;
749 /* Save our record */
750 $this->_current
= $obj;
754 // this find([$_id]) {{{
758 * Really simple find, which uses this object properties
761 * @return object this
763 final function find($_id = NULL)
765 $vars = get_document_vars($this);
766 $parent_class = __CLASS__
;
767 foreach ($vars as $key => $value) {
771 if ($value InstanceOf $parent_class) {
772 $this->getColumnDeference($vars, $key, $value);
773 unset($vars[$key]); /* delete old value */
777 if (is_array($_id)) {
778 $vars['_id'] = array('$in' => $_id);
783 $res = $this->_getCollection()->find($vars);
784 $this->setCursor($res);
789 // void save(bool $async) {{{
793 * This method save the current document in MongoDB. If
794 * we're modifying a document, a update is performed, otherwise
795 * the document is inserted.
797 * On updates, special operations such as $set, $pushAll, $pullAll
798 * and $unset in order to perform efficient updates
804 final function save($async=TRUE)
806 $update = isset($this->_id
) && $this->_id
InstanceOf MongoID
;
807 $conn = $this->_getCollection();
808 $document = $this->getCurrentDocument($update);
809 $object = $this->getDocumentVars();
811 if (isset($this->_id
)) {
812 $object['_id'] = $this->_id
;
815 if (count($document) == 0) {
816 return; /*nothing to do */
820 $this->triggerEvent('before_'.($update ?
'update' : 'create'), array(&$document, $object));
823 $conn->update(array('_id' => $this->_id
), $document, array('safe' => $async));
824 if (isset($document['$set'])) {
825 foreach ($document['$set'] as $key => $value) {
826 if (strpos($key, ".") === FALSE) {
827 $this->_current
[$key] = $value;
828 $this->$key = $value;
830 $keys = explode(".", $key);
832 $arr = & $this->$key;
833 $arrc = & $this->_current
[$key];
834 for ($i=1; $i < count($keys)-1; $i++
) {
835 $arr = &$arr[$keys[$i]];
836 $arrc = &$arrc[$keys[$i]];
838 $arr [ $keys[$i] ] = $value;
839 $arrc[ $keys[$i] ] = $value;
843 if (isset($document['$unset'])) {
844 foreach ($document['$unset'] as $key => $value) {
845 if (strpos($key, ".") === FALSE) {
846 unset($this->_current
[$key]);
849 $keys = explode(".", $key);
851 $arr = & $this->$key;
852 $arrc = & $this->_current
[$key];
853 for ($i=1; $i < count($keys)-1; $i++
) {
854 $arr = &$arr[$keys[$i]];
855 $arrc = &$arrc[$keys[$i]];
857 unset($arr [ $keys[$i] ]);
858 unset($arrc[ $keys[$i] ]);
863 $conn->insert($document, $async);
864 $this->setResult($document);
867 $this->triggerEvent('after_'.($update ?
'update' : 'create'), array($document, $object));
875 * Delete the current document
879 final function delete()
882 $document = array('_id' => $this->_id
);
883 if ($this->_cursor
InstanceOf MongoCursor
) {
884 $this->triggerEvent('before_delete', array($document));
885 $result = $this->_getCollection()->remove($document);
886 $this->triggerEvent('after_delete', array($document));
887 $this->setResult(array());
890 $criteria = (array) $this->_query
;
893 $this->triggerEvent('before_delete', array($document));
894 $this->_getCollection()->remove($criteria);
895 $this->triggerEvent('after_delete', array($document));
910 * This method perform multiple updates when a given
911 * criteria matchs (using where).
913 * By default the update is perform safely, but it can be
916 * After the operation is done, the criteria is deleted.
918 * @param array $value Values to set
919 * @param bool $safe Whether or not peform the operation safely
924 function update(Array $value, $safe=TRUE)
926 $this->_assertNotInQuery();
928 $criteria = (array) $this->_query
;
929 $options = array('multiple' => TRUE, 'safe' => $safe);
932 $col = $this->_getCollection();
933 $col->update($criteria, array('$set' => $value), $options);
944 * Delete the current colleciton and all its documents
948 final static function drop()
950 $class = get_called_class();
951 if ($class == __CLASS__
) {
955 $obj->triggerEvent('before_drop');
956 $result = $obj->_getCollection()->drop();
957 $obj->triggerEvent('after_drop');
958 if ($result['ok'] != 1) {
959 throw new ActiveMongo_Exception($result['errmsg']);
968 * Return the number of documents in the actual request. If
969 * we're not in a request, it will return 0.
973 final function count()
975 if ($this->valid()) {
976 return $this->_cursor
->count();
984 * This method should contain all the indexes, and shard keys
985 * needed by the current collection. This try to make
986 * installation on development environments easier.
995 * Perform a batchInsert of objects.
997 * @param array $documents Arrays of documents to insert
998 * @param bool $safe True if a safe will be performed, this means data validation, and wait for MongoDB OK reply
999 * @param bool $on_error_continue If an error happen while validating an object, if it should continue or not
1003 final public static function batchInsert(Array $documents, $safe=TRUE, $on_error_continue=TRUE)
1005 $context = get_called_class();
1007 if (__CLASS__
== $context) {
1008 throw new ActiveMongo_Exception("Invalid batchInsert usage");
1013 foreach ($documents as $id => $doc) {
1015 if (is_array($doc)) {
1017 self
::triggerEvent('before_create', array(&$doc), $context);
1018 self
::triggerEvent('before_validate', array(&$doc, $doc), $context);
1019 self
::triggerEvent('before_validate_creation', array(&$doc, $doc), $context);
1020 $documents[$id] = $doc;
1022 } catch (Exception
$e) {}
1025 if (!$on_error_continue) {
1026 throw new ActiveMongo_FilterException("Document $id is invalid");
1028 unset($documents[$id]);
1033 return self
::_getCollection()->batchInsert($documents, array("safe" => $safe));
1037 // bool addIndex(array $columns, array $options) {{{
1041 * Create an Index in the current collection.
1043 * @param array $columns L ist of columns
1044 * @param array $options Options
1048 final function addIndex($columns, $options=array())
1050 $default_options = array(
1054 if (!is_array($columns)) {
1055 $columns = array($columns => 1);
1058 foreach ($columns as $id => $name) {
1059 if (is_numeric($id)) {
1060 unset($columns[$id]);
1061 $columns[$name] = 1;
1065 foreach ($default_options as $option => $value) {
1066 if (!isset($options[$option])) {
1067 $options[$option] = $value;
1071 $collection = $this->_getCollection();
1073 return $collection->ensureIndex($columns, $options);
1077 // Array getIndexes() {{{
1079 * Return an array with all indexes
1083 final static function getIndexes()
1085 return self
::_getCollection()->getIndexInfo();
1089 // string __toString() {{{
1093 * If this object is treated as a string,
1094 * it would return its ID.
1098 function __toString()
1100 return (string)$this->getID();
1104 // array sendCmd(array $cmd) {{{
1106 * This method sends a command to the current
1109 * @param array $cmd Current command
1113 final protected function sendCmd($cmd)
1115 return $this->_getConnection()->command($cmd);
1121 // array getArray() {{{
1123 * Return the current document as an array
1124 * instead of a ActiveMongo object
1128 final function getArray()
1130 return get_document_vars($this);
1136 * Reset our Object, delete the current cursor if any, and reset
1137 * unsets the values.
1141 final function reset()
1143 if ($this->_cloned
) {
1144 throw new ActiveMongo_Exception("Cloned objects can't be reseted");
1146 $this->_properties
= NULL;
1147 $this->_cursor
= NULL;
1148 $this->_cursor_ex
= NULL;
1149 $this->_query
= NULL;
1150 $this->_sort
= NULL;
1153 $this->setResult(array());
1161 * Return if we're on an iteration and if it is still valid
1165 final function valid()
1168 if (!$this->_cursor_ex
) {
1169 if (!$this->_cursor
InstanceOf MongoCursor
) {
1172 $valid = $this->_cursor
InstanceOf MongoCursor
&& $this->_cursor
->valid();
1174 switch ($this->_cursor_ex
) {
1175 case self
::FIND_AND_MODIFY
:
1176 if ($this->_limit
> $this->_findandmodify_cnt
) {
1177 $this->_execFindAndModify();
1178 $valid = $this->_cursor_ex_value
['ok'] == 1;
1182 throw new ActiveMongo_Exception("Invalid _cursor_ex value");
1192 * Move to the next document
1196 final function next()
1198 if ($this->_cloned
) {
1199 throw new ActiveMongo_Exception("Cloned objects can't iterate");
1201 if (!$this->_cursor_ex
) {
1202 $result = $this->_cursor
->next();
1206 switch ($this->_cursor_ex
) {
1207 case self
::FIND_AND_MODIFY
:
1208 $this->_cursor_ex_value
= NULL;
1211 throw new ActiveMongo_Exception("Invalid _cursor_ex value");
1217 // this current() {{{
1219 * Return the current object, and load the current document
1220 * as this object property
1224 final function current()
1226 if (!$this->_cursor_ex
) {
1227 $this->setResult($this->_cursor
->current());
1229 switch ($this->_cursor_ex
) {
1230 case self
::FIND_AND_MODIFY
:
1231 if (count($this->_cursor_ex_value
) == 0) {
1232 $this->_execFindAndModify();
1234 $this->setResult($this->_cursor_ex_value
['value']);
1237 throw new ActiveMongo_Exception("Invalid _cursor_ex value");
1244 // bool rewind() {{{
1246 * Go to the first document
1248 final function rewind()
1250 if ($this->_cloned
) {
1251 throw new ActiveMongo_Exception("Cloned objects can't iterate");
1253 if (!$this->_cursor_ex
) {
1254 /* rely on MongoDB cursor */
1255 if (!$this->_cursor
InstanceOf MongoCursor
) {
1258 $result = $this->_cursor
->rewind();
1262 switch ($this->_cursor_ex
) {
1263 case self
::FIND_AND_MODIFY
:
1264 $this->_findandmodify_cnt
= 0;
1267 throw new ActiveMongo_Exception("Invalid _cursor_ex value");
1276 final function offsetExists($offset)
1278 return isset($this->$offset);
1281 final function offsetGet($offset)
1283 return $this->$offset;
1286 final function offsetSet($offset, $value)
1288 $this->$offset = $value;
1291 final function offsetUnset($offset)
1293 unset($this->$offset);
1299 // array getReference() {{{
1301 * ActiveMongo extended the Mongo references, adding
1302 * the concept of 'dynamic' requests, saving in the database
1303 * the current query with its options (sort, limit, etc).
1305 * This is useful to associate a document with a given
1306 * request. To undestand this better please see the 'reference'
1311 final function getReference($dynamic=FALSE)
1313 if (!$this->getID() && !$dynamic) {
1318 '$ref' => $this->CollectionName(),
1319 '$id' => $this->getID(),
1320 '$db' => $this->getDatabaseName(),
1321 'class' => get_class($this),
1325 if (!$this->_cursor
InstanceOf MongoCursor
&& $this->_cursor_ex
=== NULL) {
1329 if (!$this->_cursor
InstanceOf MongoCursor
) {
1330 throw new ActiveMongo_Exception("Only MongoDB native cursor could have dynamic references");
1333 $cursor = $this->_cursor
;
1334 if (!is_callable(array($cursor, "Info"))) {
1335 throw new Exception("Please upgrade your PECL/Mongo module to use this feature");
1337 $document['dynamic'] = array();
1338 $query = $cursor->Info();
1339 foreach ($query as $type => $value) {
1340 $document['dynamic'][$type] = $value;
1347 // void getDocumentReferences($document, &$refs) {{{
1349 * Get Current References
1351 * Inspect the current document trying to get any references,
1354 * @param array $document Current document
1355 * @param array &$refs References found in the document.
1356 * @param array $parent_key Parent key
1360 final protected function getDocumentReferences($document, &$refs, $parent_key=NULL)
1362 foreach ($document as $key => $value) {
1363 if (is_array($value)) {
1364 if (MongoDBRef
::isRef($value)) {
1365 $pkey = $parent_key;
1367 $refs[] = array('ref' => $value, 'key' => $pkey);
1369 $parent_key1 = $parent_key;
1370 $parent_key1[] = $key;
1371 $this->getDocumentReferences($value, $refs, $parent_key1);
1378 // object _deferencingCreateObject(string $class) {{{
1380 * Called at deferencig time
1382 * Check if the given string is a class, and it is a sub class
1383 * of ActiveMongo, if it is instance and return the object.
1385 * @param string $class
1389 private function _deferencingCreateObject($class)
1391 if (!is_subclass_of($class, __CLASS__
)) {
1392 throw new ActiveMongo_Exception("Fatal Error, imposible to create ActiveMongo object of {$class}");
1398 // void _deferencingRestoreProperty(array &$document, array $keys, mixed $req) {{{
1400 * Called at deferencig time
1402 * This method iterates $document until it could match $keys path, and
1403 * replace its value by $req.
1405 * @param array &$document Document to replace
1406 * @param array $keys Path of property to change
1407 * @param mixed $req Value to replace.
1411 private function _deferencingRestoreProperty(&$document, $keys, $req)
1415 /* find the $req proper spot */
1416 foreach ($keys as $key) {
1417 $obj = & $obj[$key];
1422 /* Delete reference variable */
1427 // object _deferencingQuery($request) {{{
1429 * Called at deferencig time
1431 * This method takes a dynamic reference and request
1434 * @param array $request Dynamic reference
1438 private function _deferencingQuery($request)
1440 $collection = $this->_getCollection();
1441 $cursor = $collection->find($request['query'], $request['fields']);
1442 if ($request['limit'] > 0) {
1443 $cursor->limit($request['limit']);
1445 if ($request['skip'] > 0) {
1446 $cursor->skip($request['skip']);
1449 $this->setCursor($cursor);
1455 // void doDeferencing() {{{
1457 * Perform a deferencing in the current document, if there is
1460 * ActiveMongo will do its best to group references queries as much
1461 * as possible, in order to perform as less request as possible.
1463 * ActiveMongo doesn't rely on MongoDB references, but it can support
1464 * it, but it is prefered to use our referencing.
1468 final function doDeferencing($refs=array())
1470 /* Get current document */
1471 $document = get_document_vars($this);
1473 if (count($refs)==0) {
1474 /* Inspect the whole document */
1475 $this->getDocumentReferences($document, $refs);
1478 $db = $this->_getConnection();
1480 /* Gather information about ActiveMongo Objects
1481 * that we need to create
1484 foreach ($refs as $ref) {
1485 if (!isset($ref['ref']['class'])) {
1487 /* Support MongoDBRef, we do our best to be compatible {{{ */
1488 /* MongoDB 'normal' reference */
1490 $obj = MongoDBRef
::get($db, $ref['ref']);
1492 /* Offset the current document to the right spot */
1493 /* Very inefficient, never use it, instead use ActiveMongo References */
1495 $this->_deferencingRestoreProperty($document, $ref['key'], $obj);
1497 /* Dirty hack, override our current document
1498 * property with the value itself, in order to
1499 * avoid replace a MongoDB reference by its content
1501 $this->_deferencingRestoreProperty($this->_current
, $ref['key'], $obj);
1507 if (isset($ref['ref']['dynamic'])) {
1508 /* ActiveMongo Dynamic Reference */
1510 /* Create ActiveMongo object */
1511 $req = $this->_deferencingCreateObject($ref['ref']['class']);
1513 /* Restore saved query */
1514 $req->_deferencingQuery($ref['ref']['dynamic']);
1518 /* Add the result set */
1519 foreach ($req as $result) {
1520 $results[] = clone $result;
1523 /* add information about the current reference */
1524 foreach ($ref['ref'] as $key => $value) {
1525 $results[$key] = $value;
1528 $this->_deferencingRestoreProperty($document, $ref['key'], $results);
1531 /* ActiveMongo Reference FTW! */
1532 $classes[$ref['ref']['class']][] = $ref;
1537 /* {{{ Create needed objects to query MongoDB and replace
1538 * our references by its objects documents.
1540 foreach ($classes as $class => $refs) {
1541 $req = $this->_deferencingCreateObject($class);
1543 /* Load list of IDs */
1545 foreach ($refs as $ref) {
1546 $ids[] = $ref['ref']['$id'];
1549 /* Search to MongoDB once for all IDs found */
1553 /* Replace our references by its objects */
1554 foreach ($refs as $ref) {
1555 $id = $ref['ref']['$id'];
1556 $place = $ref['key'];
1558 while ($req->getID() != $id && $req->next());
1560 $this->_deferencingRestoreProperty($document, $place, clone $req);
1565 /* Release request, remember we
1572 /* Replace the current document by the new deferenced objects */
1573 foreach ($document as $key => $value) {
1574 $this->$key = $value;
1579 // void getColumnDeference(&$document, $propety, ActiveMongo Obj) {{{
1581 * Prepare a "selector" document to search treaing the property
1582 * as a reference to the given ActiveMongo object.
1585 final function getColumnDeference(&$document, $property, ActiveMongo
$obj)
1587 $document["{$property}.\$id"] = $obj->getID();
1591 // void findReferences(&$document) {{{
1593 * Check if in the current document to insert or update
1594 * exists any references to other ActiveMongo Objects.
1598 final function findReferences(&$document)
1600 if (!is_array($document)) {
1603 foreach($document as &$value) {
1604 $parent_class = __CLASS__
;
1605 if (is_array($value)) {
1606 if (MongoDBRef
::isRef($value)) {
1607 /* If the property we're inspecting is a reference,
1608 * we need to remove the values, restoring the valid
1612 '$ref'=>1, '$id'=>1, '$db'=>1, 'class'=>1, 'dynamic'=>1
1614 foreach (array_keys($value) as $key) {
1615 if (!isset($arr[$key])) {
1616 unset($value[$key]);
1620 $this->findReferences($value);
1622 } else if ($value InstanceOf $parent_class) {
1623 $value = $value->getReference();
1626 /* trick: delete last var. reference */
1631 // void __clone() {{{
1633 * Cloned objects are rarely used, but ActiveMongo
1634 * uses it to create different objects per everyrecord,
1635 * which is used at deferencing. Therefore cloned object
1636 * do not contains the recordset, just the actual document,
1637 * so iterations are not allowed.
1640 final function __clone()
1642 if (!$this->_current
) {
1643 throw new ActiveMongo_Exception("Empty objects can't be cloned");
1645 unset($this->_cursor
);
1646 $this->_cloned
= TRUE;
1652 // GET DOCUMENT ID {{{
1656 * Return the current document ID. If there is
1657 * no document it would return FALSE.
1659 * @return object|FALSE
1661 final public function getID()
1663 if ($this->_id
instanceof MongoID
) {
1672 * Return the current key
1676 final function key()
1678 return (string)$this->getID();
1684 // Fancy (and silly) query abstraction {{{
1686 // _assertNotInQuery() {{{
1688 * Check if we can modify the query or not. We cannot modify
1689 * the query if we're iterating over and oldest query, in this case the
1690 * object must be reset.
1694 final private function _assertNotInQuery()
1696 if ($this->_cloned ||
$this->_cursor
InstanceOf MongoCursor ||
$this->_cursor_ex
!= NULL) {
1697 throw new ActiveMongo_Exception("You cannot modify the query, please reset the object");
1702 // bool servedFromCache() {{{
1704 * Return True if the current result
1705 * was provided by a before_query hook (aka cache)
1706 * or False if it was retrieved from MongoDB
1710 final function servedFromCache()
1712 return $this->_cached
;
1718 * Build the current request and send it to MongoDB.
1722 final function doQuery($use_cache=TRUE)
1724 if ($this->_cursor_ex
) {
1725 switch ($this->_cursor_ex
) {
1726 case self
::FIND_AND_MODIFY
:
1727 $this->_cursor_ex_value
= NULL;
1730 throw new ActiveMongo_Exception("Invalid _cursor_ex value");
1733 $this->_assertNotInQuery();
1736 'collection' => $this->CollectionName(),
1737 'query' => (array)$this->_query
,
1738 'properties' => (array)$this->_properties
,
1739 'sort' => (array)$this->_sort
,
1740 'skip' => $this->_skip
,
1741 'limit' => $this->_limit
1744 $this->_cached
= FALSE;
1746 self
::triggerEvent('before_query', array(&$query, &$documents, $use_cache));
1748 if ($documents InstanceOf MongoCursor
&& $use_cache) {
1749 $this->_cached
= TRUE;
1750 $this->setCursor($documents);
1754 $col = $this->_getCollection();
1755 if (count($query['properties']) > 0) {
1756 $cursor = $col->find($query['query'], $query['properties']);
1758 $cursor = $col->find($query['query']);
1760 if (count($query['sort']) > 0) {
1761 $cursor->sort($query['sort']);
1763 if ($query['limit'] > 0) {
1764 $cursor->limit($query['limit']);
1766 if ($query['skip'] > 0) {
1767 $cursor->skip($query['skip']);
1770 self
::triggerEvent('after_query', array($query, $cursor));
1772 /* Our cursor must be sent to ActiveMongo */
1773 $this->setCursor($cursor);
1779 // properties($props) {{{
1781 * Select 'properties' or 'columns' to be included in the document,
1782 * by default all properties are included.
1784 * @param array $props
1788 final function properties($props)
1790 $this->_assertNotInQuery();
1792 if (!is_array($props) && !is_string($props)) {
1796 if (is_string($props)) {
1797 $props = explode(",", $props);
1800 foreach ($props as $id => $name) {
1801 $props[trim($name)] = 1;
1806 /* _id should always be included */
1809 $this->_properties
= $props;
1814 final function columns($properties)
1816 return $this->properties($properties);
1820 // where($property, $value) {{{
1822 * Where abstraction.
1825 final function where($property_str, $value=NULL)
1827 $this->_assertNotInQuery();
1829 if (is_array($property_str)) {
1830 if ($value != NULL) {
1831 throw new ActiveMongo_Exception("Invalid parameters");
1833 foreach ($property_str as $property => $value) {
1834 if (is_numeric($property)) {
1838 $this->where($property, $value);
1843 $column = explode(" ", trim($property_str));
1844 if (count($column) != 1 && count($column) != 2) {
1845 throw new ActiveMongo_Exception("Failed while parsing '{$property_str}'");
1846 } else if (count($column) == 2) {
1849 switch (strtolower($column[1])) {
1873 if (is_array($value)) {
1875 $exp_scalar = FALSE;
1884 if (is_array($value)) {
1886 $exp_scalar = FALSE;
1896 $exp_scalar = FALSE;
1901 if ($value === NULL) {
1910 $value = new MongoRegex($value);
1917 $exp_scalar = FALSE;
1923 $exp_scalar = FALSE;
1928 /* geo operations */
1932 $exp_scalar = FALSE;
1936 throw new ActiveMongo_Exception("Failed to parse '{$column[1]}'");
1939 if ($exp_scalar && is_array($value)) {
1940 throw new ActiveMongo_Exception("Cannot use comparing operations with Array");
1941 } else if (!$exp_scalar && !is_array($value)) {
1942 throw new ActiveMongo_Exception("The operation {$column[1]} expected an Array");
1946 $value = array($op => $value);
1948 } else if (is_array($value)) {
1949 $value = array('$in' => $value);
1952 $spot = & $this->_query
[$column[0]];
1953 if (is_array($spot) && is_array($value)) {
1954 $spot[key($value)] = current($value);
1956 /* simulate AND among same properties if
1957 * multiple values is passed for same property
1960 if (is_array($spot)) {
1961 $spot['$all'][] = $value;
1963 $spot = array('$all' => array($spot, $value));
1974 // sort($sort_str) {{{
1976 * Abstract the documents sorting.
1978 * @param string $sort_str List of properties to use as sorting
1982 final function sort($sort_str)
1984 $this->_assertNotInQuery();
1986 $this->_sort
= array();
1987 foreach ((array)explode(",", $sort_str) as $sort_part_str) {
1988 $sort_part = explode(" ", trim($sort_part_str), 2);
1989 switch(count($sort_part)) {
1991 $sort_part[1] = 'ASC';
1996 throw new ActiveMongo_Exception("Don't know how to parse {$sort_part_str}");
1999 /* Columns name can't be empty */
2000 if (!trim($sort_part[0])) {
2001 throw new ActiveMongo_Exception("Don't know how to parse {$sort_part_str}");
2004 switch (strtoupper($sort_part[1])) {
2012 throw new ActiveMongo_Exception("Invalid sorting direction `{$sort_part[1]}`");
2014 $this->_sort
[ $sort_part[0] ] = $sort_part[1];
2021 // limit($limit, $skip) {{{
2023 * Abstract the limitation and pagination of documents.
2025 * @param int $limit Number of max. documents to retrieve
2026 * @param int $skip Number of documents to skip
2030 final function limit($limit=0, $skip=0)
2032 $this->_assertNotInQuery();
2034 if ($limit < 0 ||
$skip < 0) {
2037 $this->_limit
= $limit;
2038 $this->_skip
= $skip;
2044 // FindAndModify(Array $document) {{{
2050 final function findAndModify($document)
2052 $this->_assertNotInQuery();
2054 if (count($document) === 0) {
2055 throw new ActiveMongo_Exception("Empty \$document is not allowed");
2058 $this->_cursor_ex
= self
::FIND_AND_MODIFY
;
2059 $this->_findandmodify
= $document;
2064 private function _execFindAndModify()
2066 $query = (array)$this->_query
;
2069 "findandmodify" => $this->CollectionName(),
2071 "update" => array('$set' => $this->_findandmodify
),
2074 if (isset($this->_sort
)) {
2075 $query["sort"] = $this->_sort
;
2077 $this->_cursor_ex_value
= $this->sendCMD($query);
2079 $this->_findandmodify_cnt++
;
2087 * Return a list of properties to serialize, to save
2094 return array_keys(get_document_vars($this));
2100 require_once dirname(__FILE__
)."/Validators.php";
2101 require_once dirname(__FILE__
)."/Exceptions.php";
2108 * vim600: sw=4 ts=4 fdm=marker
2109 * vim<600: sw=4 ts=4