- Fixed minor bugs
[activemongo.git] / lib / ActiveMongo.php
blobc5a729341c2f4e6308e586c3c1fd26b44bc2dc79
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 // Class FilterException {{{
39 class ActiveMongo_Exception extends Exception
42 /**
43 * FilterException
45 * This is Exception is thrown if any validation
46 * fails when save() is called.
49 class ActiveMongo_FilterException extends ActiveMongo_Exception
52 // }}}
54 // array get_object_vars_ex(stdobj $obj) {{{
55 /**
56 * Simple hack to avoid get private and protected variables
58 * @param obj
60 * @return array
62 function get_object_vars_ex($obj)
64 return get_object_vars($obj);
66 // }}}
68 /**
69 * ActiveMongo
71 * Simple ActiveRecord pattern built on top of MongoDB. This class
72 * aims to provide easy iteration, data validation before update,
73 * and efficient update.
75 * @author César D. Rodas <crodas@php.net>
76 * @license PHP License
77 * @package ActiveMongo
78 * @version 1.0
81 abstract class ActiveMongo implements Iterator
84 // properties {{{
85 /**
86 * Current databases objects
88 * @type array
90 private static $_dbs;
91 /**
92 * Current collections objects
94 * @type array
96 private static $_collections;
97 /**
98 * Current connection to MongoDB
100 * @type MongoConnection
102 private static $_conn;
104 * Database name
106 * @type string
108 private static $_db;
110 * List of events handlers
112 * @type array
114 static private $_events = array();
116 * List of global events handlers
118 * @type array
120 static private $_super_events = array();
122 * Host name
124 * @type string
126 private static $_host;
128 * Current document
130 * @type array
132 private $_current = array();
134 * Result cursor
136 * @type MongoCursor
138 private $_cursor = null;
140 /* {{{ Silly but useful query abstraction */
141 private $_query = null;
142 private $_sort = null;
143 private $_limit = 0;
144 private $_skip = 0;
145 private $_properties = null;
146 /* }}} */
149 * Current document ID
151 * @type MongoID
153 private $_id;
156 * Tell if the current object
157 * is cloned or not.
159 * @type bool
161 private $_cloned = false;
162 // }}}
164 // GET CONNECTION CONFIG {{{
166 // string getCollectionName() {{{
168 * Get Collection Name, by default the class name,
169 * but you it can be override at the class itself to give
170 * a custom name.
172 * @return string Collection Name
174 protected function getCollectionName()
176 return strtolower(get_class($this));
178 // }}}
180 // string getDatabaseName() {{{
182 * Get Database Name, by default it is used
183 * the db name set by ActiveMong::connect()
185 * @return string DB Name
187 protected function getDatabaseName()
189 if (is_null(self::$_db)) {
190 throw new MongoException("There is no information about the default DB name");
192 return self::$_db;
194 // }}}
196 // void install() {{{
198 * Install.
200 * This static method iterate over the classes lists,
201 * and execute the setup() method on every ActiveMongo
202 * subclass. You should do this just once.
205 final public static function install()
207 $classes = array_reverse(get_declared_classes());
208 foreach ($classes as $class)
210 if ($class == __CLASS__) {
211 break;
213 if (is_subclass_of($class, __CLASS__)) {
214 $obj = new $class;
215 $obj->setup();
219 // }}}
221 // void connection($db, $host) {{{
223 * Connect
225 * This method setup parameters to connect to a MongoDB
226 * database. The connection is done when it is needed.
228 * @param string $db Database name
229 * @param string $host Host to connect
231 * @return void
233 final public static function connect($db, $host='localhost')
235 self::$_host = $host;
236 self::$_db = $db;
238 // }}}
240 // MongoConnection _getConnection() {{{
242 * Get Connection
244 * Get a valid database connection
246 * @return MongoConnection
248 final protected function _getConnection()
250 if (is_null(self::$_conn)) {
251 if (is_null(self::$_host)) {
252 self::$_host = 'localhost';
254 self::$_conn = new Mongo(self::$_host);
256 $dbname = $this->getDatabaseName();
257 if (!isSet(self::$_dbs[$dbname])) {
258 self::$_dbs[$dbname] = self::$_conn->selectDB($dbname);
260 return self::$_dbs[$dbname];
262 // }}}
264 // MongoCollection _getCollection() {{{
266 * Get Collection
268 * Get a collection connection.
270 * @return MongoCollection
272 final protected function _getCollection()
274 $colName = $this->getCollectionName();
275 if (!isset(self::$_collections[$colName])) {
276 self::$_collections[$colName] = self::_getConnection()->selectCollection($colName);
278 return self::$_collections[$colName];
280 // }}}
282 // }}}
284 // GET DOCUMENT TO SAVE OR UPDATE {{{
286 // bool getCurrentSubDocument(array &$document, string $parent_key, array $values, array $past_values) {{{
288 * Generate Sub-document
290 * This method build the difference between the current sub-document,
291 * and the origin one. If there is no difference, it would do nothing,
292 * otherwise it would build a document containing the differences.
294 * @param array &$document Document target
295 * @param string $parent_key Parent key name
296 * @param array $values Current values
297 * @param array $past_values Original values
299 * @return false
301 final function getCurrentSubDocument(&$document, $parent_key, Array $values, Array $past_values)
304 * The current property is a embedded-document,
305 * now we're looking for differences with the
306 * previous value (because we're on an update).
308 * It behaves exactly as getCurrentDocument,
309 * but this is simples (it doesn't support
310 * yet filters)
312 foreach ($values as $key => $value) {
313 $super_key = "{$parent_key}.{$key}";
314 if (is_array($value)) {
316 * Inner document detected
318 if (!isset($past_values[$key]) || !is_array($past_values[$key])) {
320 * We're lucky, it is a new sub-document,
321 * we simple add it
323 $document['$set'][$super_key] = $value;
324 } else {
326 * This is a document like this, we need
327 * to find out the differences to avoid
328 * network overhead.
330 if (!$this->getCurrentSubDocument($document, $super_key, $value, $past_values[$key])) {
331 return false;
334 continue;
335 } else if (!isset($past_values[$key]) || $past_values[$key] != $value) {
336 $document['$set'][$super_key] = $value;
340 foreach (array_diff(array_keys($past_values), array_keys($values)) as $key) {
341 $super_key = "{$parent_key}.{$key}";
342 $document['$unset'][$super_key] = 1;
345 return true;
347 // }}}
349 // array getCurrentDocument(bool $update) {{{
351 * Get Current Document
353 * Based on this object properties a new document (Array)
354 * is returned. If we're modifying an document, just the modified
355 * properties are included in this document, which uses $set,
356 * $unset, $pushAll and $pullAll.
359 * @param bool $update
361 * @return array
363 final protected function getCurrentDocument($update=false, $current=false)
365 $document = array();
366 $object = get_object_vars_ex($this);
368 if (!$current) {
369 $current = (array)$this->_current;
373 $this->findReferences($object);
375 $this->triggerEvent('before_validate_'.($update?'update':'creation'), array(&$object));
376 $this->triggerEvent('before_validate', array(&$object));
378 foreach ($object as $key => $value) {
379 if (!$value) {
380 continue;
382 if ($update) {
383 if (is_array($value) && isset($current[$key])) {
385 * If the Field to update is an array, it has a different
386 * behaviour other than $set and $unset. Fist, we need
387 * need to check if it is an array or document, because
388 * they can't be mixed.
391 if (!is_array($current[$key])) {
393 * We're lucky, the field wasn't
394 * an array previously.
396 $this->runFilter($key, $value, $current[$key]);
397 $document['$set'][$key] = $value;
398 continue;
401 if (!$this->getCurrentSubDocument($document, $key, $value, $current[$key])) {
402 throw new Exception("{$key}: Array and documents are not compatible");
404 } else if(!isset($current[$key]) || $value !== $current[$key]) {
406 * It is 'linear' field that has changed, or
407 * has been modified.
409 $past_value = isset($current[$key]) ? $current[$key] : null;
410 $this->runFilter($key, $value, $past_value);
411 $document['$set'][$key] = $value;
413 } else {
415 * It is a document insertation, so we
416 * create the document.
418 $this->runFilter($key, $value, null);
419 $document[$key] = $value;
423 /* Updated behaves in a diff. way */
424 if ($update) {
425 foreach (array_diff(array_keys($this->_current), array_keys($object)) as $property) {
426 if ($property == '_id') {
427 continue;
429 $document['$unset'][$property] = 1;
433 if (count($document) == 0) {
434 return array();
437 $this->triggerEvent('after_validate_'.($update?'update':'creation'), array(&$object));
438 $this->triggerEvent('after_validate', array(&$document));
440 return $document;
442 // }}}
444 // }}}
446 // EVENT HANDLERS {{{
448 // addEvent($action, $callback) {{{
450 * addEvent
453 final static function addEvent($action, $callback)
455 if (!is_callable($callback)) {
456 throw new Exception("Invalid callback");
459 $class = get_called_class();
460 if ($class == __CLASS__) {
461 $events = & self::$_super_events;
462 } else {
463 $events = & self::$_events[$class];
465 if (!isset($events[$action])) {
466 $events[$action] = array();
468 $events[$action][] = $callback;
469 return true;
471 // }}}
473 // triggerEvent(string $event, Array $events_params) {{{
474 final function triggerEvent($event, Array $events_params = array())
476 $events = & self::$_events[get_class($this)][$event];
477 $sevents = & self::$_super_events[$event];
479 if (!is_array($events_params)) {
480 return false;
483 /* Super-Events handler receives the ActiveMongo class name as first param */
484 $sevents_params = array_merge(array(get_class($this)), $events_params);
486 foreach (array('events', 'sevents') as $event_type) {
487 if (count($$event_type) > 0) {
488 $params = "{$event_type}_params";
489 foreach ($$event_type as $fnc) {
490 call_user_func_array($fnc, $$params);
495 /* Some natives events are allowed to be called
496 * as methods, if they exists
498 switch ($event) {
499 case 'before_create':
500 case 'before_update':
501 case 'before_validate':
502 case 'before_delete':
503 case 'after_create':
504 case 'after_update':
505 case 'after_validate':
506 case 'after_delete':
507 $fnc = array($this, $event);
508 $params = "events_params";
509 if (is_callable($fnc)) {
510 call_user_func_array($fnc, $$params);
512 break;
515 // }}}
517 // void runFilter(string $key, mixed &$value, mixed $past_value) {{{
519 * *Internal Method*
521 * This method check if the current document property has
522 * a filter method, if so, call it.
524 * If the filter returns false, throw an Exception.
526 * @return void
528 protected function runFilter($key, &$value, $past_value)
530 $filter = array($this, "{$key}_filter");
531 if (is_callable($filter)) {
532 $filter = call_user_func_array($filter, array(&$value, $past_value));
533 if ($filter===false) {
534 throw new ActiveMongo_FilterException("{$key} filter failed");
536 $this->$key = $value;
539 // }}}
541 // }}}
543 // void setCursor(MongoCursor $obj) {{{
545 * Set Cursor
547 * This method receive a MongoCursor and make
548 * it iterable.
550 * @param MongoCursor $obj
552 * @return void
554 final protected function setCursor(MongoCursor $obj)
556 $this->_cursor = $obj;
557 $this->setResult($obj->getNext());
559 // }}}
561 // void setResult(Array $obj) {{{
563 * Set Result
565 * This method takes an document and copy it
566 * as properties in this object.
568 * @param Array $obj
570 * @return void
572 final protected function setResult($obj)
574 /* Unsetting previous results, if any */
575 foreach (array_keys((array)$this->_current) as $key) {
576 unset($this->$key);
579 /* Add our current resultset as our object's property */
580 foreach ((array)$obj as $key => $value) {
581 if ($key[0] == '$') {
582 continue;
584 $this->$key = $value;
587 /* Save our record */
588 $this->_current = $obj;
590 // }}}
592 // this find([$_id]) {{{
594 * Simple find.
596 * Really simple find, which uses this object properties
597 * for fast filtering
599 * @return object this
601 final function find($_id = null)
603 $vars = get_object_vars_ex($this);
604 foreach ($vars as $key => $value) {
605 if (!$value) {
606 unset($vars[$key]);
608 $parent_class = __CLASS__;
609 if ($value InstanceOf $parent_class) {
610 $this->getColumnDeference($vars, $key, $value);
611 unset($vars[$key]); /* delete old value */
614 if ($_id != null) {
615 if (is_array($_id)) {
616 $vars['_id'] = array('$in' => $_id);
617 } else {
618 $vars['_id'] = $_id;
621 $res = $this->_getCollection()->find($vars);
622 $this->setCursor($res);
623 return $this;
625 // }}}
627 // void save(bool $async) {{{
629 * Save
631 * This method save the current document in MongoDB. If
632 * we're modifying a document, a update is performed, otherwise
633 * the document is inserted.
635 * On updates, special operations such as $set, $pushAll, $pullAll
636 * and $unset in order to perform efficient updates
638 * @param bool $async
640 * @return void
642 final function save($async=true)
644 $update = isset($this->_id) && $this->_id InstanceOf MongoID;
645 $conn = $this->_getCollection();
646 $obj = $this->getCurrentDocument($update);
647 if (count($obj) == 0) {
648 return; /*nothing to do */
651 /* PRE-save hook */
652 $this->triggerEvent('before_'.($update ? 'update' : 'create'), array(&$obj));
654 if ($update) {
655 $conn->update(array('_id' => $this->_id), $obj);
656 foreach ($obj as $key => $value) {
657 if ($key[0] == '$') {
658 continue;
660 $this->_current[$key] = $value;
662 } else {
663 $conn->insert($obj, $async);
664 $this->_id = $obj['_id'];
665 $this->_current = $obj;
668 $this->triggerEvent('after_'.($update ? 'update' : 'create'), array($obj));
670 // }}}
672 // bool delete() {{{
674 * Delete the current document
676 * @return bool
678 final function delete()
680 if ($this->valid()) {
681 $document = array('_id' => $this->_id);
682 $this->triggerEvent('before_delete', array($document));
683 $result = $this->_getCollection()->remove($document);
684 $this->triggerEvent('after_delete', array($document));
685 return $result;
687 return false;
689 // }}}
691 // void drop() {{{
693 * Delete the current colleciton and all its documents
695 * @return void
697 final static function drop()
699 $class = get_called_class();
700 if ($class == __CLASS__) {
701 return false;
703 $obj = new $class;
704 return $obj->_getCollection()->drop();
706 // }}}
708 // int count() {{{
710 * Return the number of documents in the actual request. If
711 * we're not in a request, it will return 0.
713 * @return int
715 final function count()
717 if ($this->valid()) {
718 return $this->_cursor->count();
720 return 0;
722 // }}}
724 // void setup() {{{
726 * This method should contain all the indexes, and shard keys
727 * needed by the current collection. This try to make
728 * installation on development environments easier.
730 function setup()
733 // }}}
735 // bool addIndex(array $columns, array $options) {{{
737 * addIndex
739 * Create an Index in the current collection.
741 * @param array $columns L ist of columns
742 * @param array $options Options
744 * @return bool
746 final function addIndex($columns, $options=array())
748 $default_options = array(
749 'background' => 1,
752 foreach ($default_options as $option => $value) {
753 if (!isset($options[$option])) {
754 $options[$option] = $value;
758 $collection = $this->_getCollection();
760 return $collection->ensureIndex($columns, $options);
762 // }}}
764 // string __toString() {{{
766 * To String
768 * If this object is treated as a string,
769 * it would return its ID.
771 * @return string
773 final function __toString()
775 return (string)$this->getID();
777 // }}}
779 // array sendCmd(array $cmd) {{{
781 * This method sends a command to the current
782 * database.
784 * @param array $cmd Current command
786 * @return array
788 final protected function sendCmd($cmd)
790 return $this->_getConnection()->command($cmd);
792 // }}}
794 // ITERATOR {{{
796 // void reset() {{{
798 * Reset our Object, delete the current cursor if any, and reset
799 * unsets the values.
801 * @return void
803 final function reset()
805 $this->_properties = null;
806 $this->_cursor = null;
807 $this->_query = null;
808 $this->_sort = null;
809 $this->_limit = 0;
810 $this->_skip = 0;
811 $this->setResult(array());
813 // }}}
815 // bool valid() {{{
817 * Valid
819 * Return if we're on an iteration and if it is still valid
821 * @return true
823 final function valid()
825 if (!$this->_cursor InstanceOf MongoCursor) {
826 $this->doQuery();
828 return $this->_cursor InstanceOf MongoCursor && $this->_cursor->valid();
830 // }}}
832 // bool next() {{{
834 * Move to the next document
836 * @return bool
838 final function next()
840 if ($this->_cloned) {
841 throw new MongoException("Cloned objects can't iterate");
843 return $this->_cursor->next();
845 // }}}
847 // this current() {{{
849 * Return the current object, and load the current document
850 * as this object property
852 * @return object
854 final function current()
856 $this->setResult($this->_cursor->current());
857 return $this;
859 // }}}
861 // bool rewind() {{{
863 * Go to the first document
865 final function rewind()
867 if (!$this->_cursor InstanceOf MongoCursor) {
868 $this->doQuery();
870 return $this->_cursor->rewind();
872 // }}}
874 // }}}
876 // REFERENCES {{{
878 // array getReference() {{{
880 * ActiveMongo extended the Mongo references, adding
881 * the concept of 'dynamic' requests, saving in the database
882 * the current query with its options (sort, limit, etc).
884 * This is useful to associate a document with a given
885 * request. To undestand this better please see the 'reference'
886 * example.
888 * @return array
890 final function getReference($dynamic=false)
892 if (!$this->getID() && !$dynamic) {
893 return null;
896 $document = array(
897 '$ref' => $this->getCollectionName(),
898 '$id' => $this->getID(),
899 '$db' => $this->getDatabaseName(),
900 'class' => get_class($this),
903 if ($dynamic && $this->_cursor InstanceOf MongoCursor) {
904 $cursor = $this->_cursor;
905 if (!is_callable(array($cursor, "Info"))) {
906 throw new Exception("Please upgrade your PECL/Mongo module to use this feature");
908 $document['dynamic'] = array();
909 $query = $cursor->Info();
910 foreach ($query as $type => $value) {
911 $document['dynamic'][$type] = $value;
914 return $document;
916 // }}}
918 // void getDocumentReferences($document, &$refs) {{{
920 * Get Current References
922 * Inspect the current document trying to get any references,
923 * if any.
925 * @param array $document Current document
926 * @param array &$refs References found in the document.
927 * @param array $parent_key Parent key
929 * @return void
931 final protected function getDocumentReferences($document, &$refs, $parent_key=null)
933 foreach ($document as $key => $value) {
934 if (is_array($value)) {
935 if (MongoDBRef::isRef($value)) {
936 $pkey = $parent_key;
937 $pkey[] = $key;
938 $refs[] = array('ref' => $value, 'key' => $pkey);
939 } else {
940 $parent_key[] = $key;
941 $this->getDocumentReferences($value, $refs, $parent_key);
946 // }}}
948 // object _deferencingCreateObject(string $class) {{{
950 * Called at deferencig time
952 * Check if the given string is a class, and it is a sub class
953 * of ActiveMongo, if it is instance and return the object.
955 * @param string $class
957 * @return object
959 private function _deferencingCreateObject($class)
961 if (!is_subclass_of($class, __CLASS__)) {
962 throw new MongoException("Fatal Error, imposible to create ActiveMongo object of {$class}");
964 return new $class;
966 // }}}
968 // void _deferencingRestoreProperty(array &$document, array $keys, mixed $req) {{{
970 * Called at deferencig time
972 * This method iterates $document until it could match $keys path, and
973 * replace its value by $req.
975 * @param array &$document Document to replace
976 * @param array $keys Path of property to change
977 * @param mixed $req Value to replace.
979 * @return void
981 private function _deferencingRestoreProperty(&$document, $keys, $req)
983 $obj = & $document;
985 /* find the $req proper spot */
986 foreach ($keys as $key) {
987 $obj = & $obj[$key];
990 $obj = $req;
992 /* Delete reference variable */
993 unset($obj);
995 // }}}
997 // object _deferencingQuery($request) {{{
999 * Called at deferencig time
1001 * This method takes a dynamic reference and request
1002 * it to MongoDB.
1004 * @param array $request Dynamic reference
1006 * @return this
1008 private function _deferencingQuery($request)
1010 $collection = $this->_getCollection();
1011 $cursor = $collection->find($request['query'], $request['fields']);
1012 if ($request['limit'] > 0) {
1013 $cursor->limit($request['limit']);
1015 if ($request['skip'] > 0) {
1016 $cursor->limit($request['limit']);
1019 $this->setCursor($cursor);
1021 return $this;
1023 // }}}
1025 // void doDeferencing() {{{
1027 * Perform a deferencing in the current document, if there is
1028 * any reference.
1030 * ActiveMongo will do its best to group references queries as much
1031 * as possible, in order to perform as less request as possible.
1033 * ActiveMongo doesn't rely on MongoDB references, but it can support
1034 * it, but it is prefered to use our referencing.
1036 * @experimental
1038 final function doDeferencing($refs=array())
1040 /* Get current document */
1041 $document = get_object_vars_ex($this);
1043 if (count($refs)==0) {
1044 /* Inspect the whole document */
1045 $this->getDocumentReferences($document, $refs);
1048 $db = $this->_getConnection();
1050 /* Gather information about ActiveMongo Objects
1051 * that we need to create
1053 $classes = array();
1054 foreach ($refs as $ref) {
1055 if (!isset($ref['ref']['class'])) {
1057 /* Support MongoDBRef, we do our best to be compatible {{{ */
1058 /* MongoDB 'normal' reference */
1060 $obj = MongoDBRef::get($db, $ref['ref']);
1062 /* Offset the current document to the right spot */
1063 /* Very inefficient, never use it, instead use ActiveMongo References */
1065 $this->_deferencingRestoreProperty($document, $ref['key'], clone $req);
1067 /* Dirty hack, override our current document
1068 * property with the value itself, in order to
1069 * avoid replace a MongoDB reference by its content
1071 $this->_deferencingRestoreProperty($this->_current, $ref['key'], clone $req);
1073 /* }}} */
1075 } else {
1077 if (isset($ref['ref']['dynamic'])) {
1078 /* ActiveMongo Dynamic Reference */
1080 /* Create ActiveMongo object */
1081 $req = $this->_deferencingCreateObject($ref['ref']['class']);
1083 /* Restore saved query */
1084 $req->_deferencingQuery($ref['ref']['dynamic']);
1086 $results = array();
1088 /* Add the result set */
1089 foreach ($req as $result) {
1090 $results[] = clone $result;
1093 /* add information about the current reference */
1094 foreach ($ref['ref'] as $key => $value) {
1095 $results[$key] = $value;
1098 $this->_deferencingRestoreProperty($document, $ref['key'], $results);
1100 } else {
1101 /* ActiveMongo Reference FTW! */
1102 $classes[$ref['ref']['class']][] = $ref;
1107 /* {{{ Create needed objects to query MongoDB and replace
1108 * our references by its objects documents.
1110 foreach ($classes as $class => $refs) {
1111 $req = $this->_deferencingCreateObject($class);
1113 /* Load list of IDs */
1114 $ids = array();
1115 foreach ($refs as $ref) {
1116 $ids[] = $ref['ref']['$id'];
1119 /* Search to MongoDB once for all IDs found */
1120 $req->find($ids);
1122 if ($req->count() != count($refs)) {
1123 $total = $req->count();
1124 $expected = count($refs);
1125 throw new MongoException("Dereferencing error, MongoDB replied {$total} objects, we expected {$expected}");
1128 /* Replace our references by its objects */
1129 foreach ($refs as $ref) {
1130 $id = $ref['ref']['$id'];
1131 $place = $ref['key'];
1132 $req->rewind();
1133 while ($req->getID() != $id && $req->next());
1135 assert($req->getID() == $id);
1137 $this->_deferencingRestoreProperty($document, $place, clone $req);
1139 unset($obj);
1142 /* Release request, remember we
1143 * safely cloned it,
1145 unset($req);
1147 // }}}
1149 /* Replace the current document by the new deferenced objects */
1150 foreach ($document as $key => $value) {
1151 $this->$key = $value;
1154 // }}}
1156 // void getColumnDeference(&$document, $propety, ActiveMongo Obj) {{{
1158 * Prepare a "selector" document to search treaing the property
1159 * as a reference to the given ActiveMongo object.
1162 final function getColumnDeference(&$document, $property, ActiveMongo $obj)
1164 $document["{$property}.\$id"] = $obj->getID();
1166 // }}}
1168 // void findReferences(&$document) {{{
1170 * Check if in the current document to insert or update
1171 * exists any references to other ActiveMongo Objects.
1173 * @return void
1175 final function findReferences(&$document)
1177 if (!is_array($document)) {
1178 return;
1180 foreach($document as &$value) {
1181 $parent_class = __CLASS__;
1182 if (is_array($value)) {
1183 if (MongoDBRef::isRef($value)) {
1184 /* If the property we're inspecting is a reference,
1185 * we need to remove the values, restoring the valid
1186 * Reference.
1188 $arr = array(
1189 '$ref'=>1, '$id'=>1, '$db'=>1, 'class'=>1, 'dynamic'=>1
1191 foreach (array_keys($value) as $key) {
1192 if (!isset($arr[$key])) {
1193 unset($value[$key]);
1196 } else {
1197 $this->findReferences($value);
1199 } else if ($value InstanceOf $parent_class) {
1200 $value = $value->getReference();
1203 /* trick: delete last var. reference */
1204 unset($value);
1206 // }}}
1208 // void __clone() {{{
1209 /**
1210 * Cloned objects are rarely used, but ActiveMongo
1211 * uses it to create different objects per everyrecord,
1212 * which is used at deferencing. Therefore cloned object
1213 * do not contains the recordset, just the actual document,
1214 * so iterations are not allowed.
1217 final function __clone()
1219 unset($this->_cursor);
1220 $this->_cloned = true;
1222 // }}}
1224 // }}}
1226 // GET DOCUMENT ID {{{
1228 // getID() {{{
1230 * Return the current document ID. If there is
1231 * no document it would return false.
1233 * @return object|false
1235 final public function getID()
1237 if ($this->_id instanceof MongoID) {
1238 return $this->_id;
1240 return false;
1242 // }}}
1244 // string key() {{{
1246 * Return the current key
1248 * @return string
1250 final function key()
1252 return $this->getID();
1254 // }}}
1256 // }}}
1258 // Fancy (and silly) query abstraction {{{
1260 // _assertNoQuery() {{{
1262 * Check if we can modify the query or not. We cannot modify
1263 * the query if we already asked to MongoDB, in this case the
1264 * object must be reset.
1266 * @return void
1268 final private function _assertNoQuery()
1270 if ($this->_cursor InstanceOf MongoCursor) {
1271 throw new ActiveMongo_Exception("You cannot modify the query, please reset the object");
1274 // }}}
1276 // doQuery() {{{
1278 * Build the current request and send it to MongoDB.
1280 * @return this
1282 final function doQuery()
1284 $this->_assertNoQuery();
1286 $col = $this->_getCollection();
1287 if (count($this->_properties) > 0) {
1288 $cursor = $col->find((array)$this->_query['query'], $this->_properties);
1289 } else {
1290 $cursor = $col->find((array)$this->_query['query']);
1292 if (is_array($this->_sort)) {
1293 $cursor->sort($this->_sort);
1295 if ($this->_limit > 0) {
1296 $cursor->limit($this->_limit);
1298 if ($this->_skip > 0) {
1299 $cursor->skip($this->_skip);
1302 /* Our cursor must be sent to ActiveMongo */
1303 $this->setCursor($cursor);
1305 return $this;
1307 // }}}
1309 // properties($props) {{{
1311 * Select 'properties' or 'columns' to be included in the document,
1312 * by default all properties are included.
1314 * @param array $props
1316 * @return this
1318 final function properties($props)
1320 $this->_assertNoQuery();
1322 if (!is_array($props) && !is_string($props)) {
1323 return false;
1326 if (is_string($props)) {
1327 $props = explode(",", $props);
1330 foreach ($props as $id => $name) {
1331 $props[trim($name)] = 1;
1332 unset($props[$id]);
1335 $this->_properties = $props;
1337 return $this;
1340 final function columns($properties)
1342 return $this->properties($properties);
1344 // }}}
1346 // where($property, $value) {{{
1348 * Where abstraction.
1351 final function where($property_str, $value)
1353 $this->_assertNoQuery();
1355 $column = explode(" ", $property_str);
1356 if (count($column) != 1 && count($column) != 2) {
1357 throw new ActiveMongo_Exception("Failed while parsing '{$property_str}'");
1358 } else if (count($column) == 2) {
1359 if (is_array($value) && $column[1] != 'near') {
1360 throw new ActiveMongo_Exception("Cannot use comparing operations with Array");
1362 switch ($column[1]) {
1363 case '>':
1364 $op = '$gt';
1365 break;
1366 case '>=':
1367 $op = '$gte';
1368 case '<':
1369 $op = '$lt';
1370 break;
1371 case '<=':
1372 $op = '$lte';
1373 break;
1374 case '==':
1375 $op = '$eq';
1376 case '!=':
1377 $op = '$ne';
1378 break;
1379 case 'near':
1380 $op = '$near';
1381 break;
1382 default:
1383 throw new ActiveMongo_Exception("Failed to parse '{$column[1]}'");
1385 $value = array($op => $value);
1386 } else if (is_array($value)) {
1387 $value = array('$in' => $value);
1390 $this->_query['query'][$column[0]] = $value;
1392 return $this;
1394 // }}}
1396 // sort($sort_str) {{{
1398 * Abstract the documents sorting.
1400 * @param string $sort_str List of properties to use as sorting
1402 * @return this
1404 final function sort($sort_str)
1406 $this->_assertNoQuery();
1408 $this->_sort = array();
1409 foreach ((array)explode(",", $sort_str) as $sort_part_str) {
1410 $sort_part = explode(" ", trim($sort_part_str), 2);
1411 switch(count($sort_part)) {
1412 case 1:
1413 $sort_part[1] = 'ASC';
1414 break;
1415 case 2:
1416 break;
1417 default:
1418 throw new ActiveMongo_Exception("Don't know how to parse {$sort_part_str}");
1421 switch (strtoupper($sort_part[1])) {
1422 case 'ASC':
1423 $sort_part[1] = 1;
1424 break;
1425 case 'DESC':
1426 $sort_part[1] = -1;
1427 break;
1428 default:
1429 throw new ActiveMongo_Exception("Invalid sorting direction `{$sort_part[1]}`");
1431 $this->_sort[ $sort_part[0] ] = $sort_part[1];
1434 return $this;
1436 // }}}
1438 // limit($limit, $skip) {{{
1440 * Abstract the limitation and pagination of documents.
1442 * @param int $limit Number of max. documents to retrieve
1443 * @param int $skip Number of documents to skip
1445 * @return this
1447 final function limit($limit=0, $skip=0)
1449 $this->_assertNoQuery();
1451 if ($limit < 0 || $skip < 0) {
1452 return false;
1454 $this->_limit = $limit;
1455 $this->_skip = $skip;
1457 return $this;
1459 // }}}
1461 // }}}
1465 require_once dirname(__FILE__)."/Validators.php";
1468 * Local variables:
1469 * tab-width: 4
1470 * c-basic-offset: 4
1471 * End:
1472 * vim600: sw=4 ts=4 fdm=marker
1473 * vim<600: sw=4 ts=4