- Fixed error with non-value (NULL, 0, false)
[activemongo.git] / lib / ActiveMongo.php
blobecec92ebc1b578d267f780d393e70921cc58926e
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 PHP License
66 * @package ActiveMongo
67 * @version 1.0
70 abstract class ActiveMongo implements Iterator, Countable, ArrayAccess
72 //{{{ Constants
73 const FIND_AND_MODIFY = 0x001;
74 // }}}
76 // properties {{{
77 /**
78 * Current databases objects
80 * @type array
82 private static $_dbs;
83 /**
84 * Current collections objects
86 * @type array
88 private static $_collections;
89 /**
90 * Current connection to MongoDB
92 * @type MongoConnection
94 private static $_conn;
95 /**
96 * Database name
98 * @type string
100 private static $_db;
102 * List of events handlers
104 * @type array
106 static private $_events = array();
108 * List of global events handlers
110 * @type array
112 static private $_super_events = array();
114 * Host name
116 * @type string
118 private static $_host;
120 * User (Auth)
122 * @type string
124 private static $_user;
127 * Password (Auth)
129 * @type string
131 private static $_pwd;
134 * Current document
136 * @type array
138 private $_current = array();
140 * Result cursor
142 * @type MongoCursor
144 private $_cursor = null;
146 * Extended result cursor, used for FindAndModify now
148 * @type int
150 private $_cursor_ex = null;
151 private $_cursor_ex_value;
153 * Count the findandmodify result counts
155 * @tyep array
157 private $_findandmodify_cnt = 0;
158 /* value to modify */
159 private $_findandmodify;
161 /* {{{ Silly but useful query abstraction */
162 private $_query = null;
163 private $_sort = null;
164 private $_limit = 0;
165 private $_skip = 0;
166 private $_properties = null;
167 /* }}} */
170 * Current document ID
172 * @type MongoID
174 private $_id;
177 * Tell if the current object
178 * is cloned or not.
180 * @type bool
182 private $_cloned = false;
183 // }}}
185 // GET CONNECTION CONFIG {{{
187 // string getCollectionName() {{{
189 * Get Collection Name, by default the class name,
190 * but you it can be override at the class itself to give
191 * a custom name.
193 * @return string Collection Name
195 protected function getCollectionName()
197 if (isset($this)) {
198 return strtolower(get_class($this));
199 } else {
200 return strtolower(get_called_class());
203 // }}}
205 // string getDatabaseName() {{{
207 * Get Database Name, by default it is used
208 * the db name set by ActiveMong::connect()
210 * @return string DB Name
212 protected function getDatabaseName()
214 if (is_null(self::$_db)) {
215 throw new MongoException("There is no information about the default DB name");
217 return self::$_db;
219 // }}}
221 // void install() {{{
223 * Install.
225 * This static method iterate over the classes lists,
226 * and execute the setup() method on every ActiveMongo
227 * subclass. You should do this just once.
230 final public static function install()
232 $classes = array_reverse(get_declared_classes());
233 foreach ($classes as $class)
235 if ($class == __CLASS__) {
236 break;
238 if (is_subclass_of($class, __CLASS__)) {
239 $obj = new $class;
240 $obj->setup();
244 // }}}
246 // void connection($db, $host) {{{
248 * Connect
250 * This method setup parameters to connect to a MongoDB
251 * database. The connection is done when it is needed.
253 * @param string $db Database name
254 * @param string $host Host to connect
255 * @param string $user User (Auth)
256 * @param string $pwd Password (Auth)
258 * @return void
260 final public static function connect($db, $host='localhost', $user = null, $pwd=null)
262 self::$_host = $host;
263 self::$_db = $db;
264 self::$_user = $user;
265 self::$_pwd = $pwd;
267 // }}}
269 // MongoConnection _getConnection() {{{
271 * Get Connection
273 * Get a valid database connection
275 * @return MongoConnection
277 final protected function _getConnection()
279 if (is_null(self::$_conn)) {
280 if (is_null(self::$_host)) {
281 self::$_host = 'localhost';
283 self::$_conn = new Mongo(self::$_host);
285 if (isset($this)) {
286 $dbname = $this->getDatabaseName();
287 } else {
288 $dbname = self::getDatabaseName();
290 if (!isSet(self::$_dbs[$dbname])) {
291 self::$_dbs[$dbname] = self::$_conn->selectDB($dbname);
293 if ( !is_null(self::$_user ) && !is_null(self::$_pwd ) ) {
295 self::$_dbs[$dbname]->authenticate( self::$_user,self::$_pwd ) ;
301 return self::$_dbs[$dbname];
303 // }}}
305 // MongoCollection _getCollection() {{{
307 * Get Collection
309 * Get a collection connection.
311 * @return MongoCollection
313 final protected function _getCollection()
315 if (isset($this)) {
316 $colName = $this->getCollectionName();
317 } else {
318 $colName = self::getCollectionName();
320 if (!isset(self::$_collections[$colName])) {
321 self::$_collections[$colName] = self::_getConnection()->selectCollection($colName);
323 return self::$_collections[$colName];
325 // }}}
327 // }}}
329 // GET DOCUMENT TO SAVE OR UPDATE {{{
331 // bool getCurrentSubDocument(array &$document, string $parent_key, array $values, array $past_values) {{{
333 * Generate Sub-document
335 * This method build the difference between the current sub-document,
336 * and the origin one. If there is no difference, it would do nothing,
337 * otherwise it would build a document containing the differences.
339 * @param array &$document Document target
340 * @param string $parent_key Parent key name
341 * @param array $values Current values
342 * @param array $past_values Original values
344 * @return false
346 final function getCurrentSubDocument(&$document, $parent_key, Array $values, Array $past_values)
349 * The current property is a embedded-document,
350 * now we're looking for differences with the
351 * previous value (because we're on an update).
353 * It behaves exactly as getCurrentDocument,
354 * but this is simples (it doesn't support
355 * yet filters)
357 foreach ($values as $key => $value) {
358 $super_key = "{$parent_key}.{$key}";
359 if (is_array($value)) {
361 * Inner document detected
363 if (!isset($past_values[$key]) || !is_array($past_values[$key])) {
365 * We're lucky, it is a new sub-document,
366 * we simple add it
368 $document['$set'][$super_key] = $value;
369 } else {
371 * This is a document like this, we need
372 * to find out the differences to avoid
373 * network overhead.
375 if (!$this->getCurrentSubDocument($document, $super_key, $value, $past_values[$key])) {
376 return false;
379 continue;
380 } else if (!isset($past_values[$key]) || $past_values[$key] != $value) {
381 $document['$set'][$super_key] = $value;
385 foreach (array_diff(array_keys($past_values), array_keys($values)) as $key) {
386 $super_key = "{$parent_key}.{$key}";
387 $document['$unset'][$super_key] = 1;
390 return true;
392 // }}}
394 // array getCurrentDocument(bool $update) {{{
396 * Get Current Document
398 * Based on this object properties a new document (Array)
399 * is returned. If we're modifying an document, just the modified
400 * properties are included in this document, which uses $set,
401 * $unset, $pushAll and $pullAll.
404 * @param bool $update
406 * @return array
408 final protected function getCurrentDocument($update=false, $current=false)
410 $document = array();
411 $object = get_document_vars($this);
413 if (!$current) {
414 $current = (array)$this->_current;
418 $this->findReferences($object);
420 $this->triggerEvent('before_validate', array(&$object));
421 $this->triggerEvent('before_validate_'.($update?'update':'creation'), array(&$object));
423 foreach ($object as $key => $value) {
424 if ($update) {
425 if (is_array($value) && isset($current[$key])) {
427 * If the Field to update is an array, it has a different
428 * behaviour other than $set and $unset. Fist, we need
429 * need to check if it is an array or document, because
430 * they can't be mixed.
433 if (!is_array($current[$key])) {
435 * We're lucky, the field wasn't
436 * an array previously.
438 $this->runFilter($key, $value, $current[$key]);
439 $document['$set'][$key] = $value;
440 continue;
443 if (!$this->getCurrentSubDocument($document, $key, $value, $current[$key])) {
444 throw new Exception("{$key}: Array and documents are not compatible");
446 } else if(!isset($current[$key]) || $value !== $current[$key]) {
448 * It is 'linear' field that has changed, or
449 * has been modified.
451 $past_value = isset($current[$key]) ? $current[$key] : null;
452 $this->runFilter($key, $value, $past_value);
453 $document['$set'][$key] = $value;
455 } else {
457 * It is a document insertation, so we
458 * create the document.
460 $this->runFilter($key, $value, null);
461 $document[$key] = $value;
465 /* Updated behaves in a diff. way */
466 if ($update) {
467 foreach (array_diff(array_keys($this->_current), array_keys($object)) as $property) {
468 if ($property == '_id') {
469 continue;
471 $document['$unset'][$property] = 1;
475 if (count($document) == 0) {
476 return array();
479 $this->triggerEvent('after_validate', array(&$document));
480 $this->triggerEvent('after_validate_'.($update?'update':'creation'), array(&$object));
482 return $document;
484 // }}}
486 // }}}
488 // EVENT HANDLERS {{{
490 // addEvent($action, $callback) {{{
492 * addEvent
495 final static function addEvent($action, $callback)
497 if (!is_callable($callback)) {
498 throw new Exception("Invalid callback");
501 $class = get_called_class();
502 if ($class == __CLASS__) {
503 $events = & self::$_super_events;
504 } else {
505 $events = & self::$_events[$class];
507 if (!isset($events[$action])) {
508 $events[$action] = array();
510 $events[$action][] = $callback;
511 return true;
513 // }}}
515 // triggerEvent(string $event, Array $events_params) {{{
516 final function triggerEvent($event, Array $events_params = array())
518 if (!isset($this)) {
519 $class = get_called_class();
520 } else {
521 $class = get_class($this);
523 $events = & self::$_events[$class][$event];
524 $sevents = & self::$_super_events[$event];
526 if (!is_array($events_params)) {
527 return false;
530 /* Super-Events handler receives the ActiveMongo class name as first param */
531 $sevents_params = array_merge(array($class), $events_params);
533 foreach (array('events', 'sevents') as $event_type) {
534 if (count($$event_type) > 0) {
535 $params = "{$event_type}_params";
536 foreach ($$event_type as $fnc) {
537 call_user_func_array($fnc, $$params);
542 /* Some natives events are allowed to be called
543 * as methods, if they exists
545 if (!isset($this)) {
546 return;
548 switch ($event) {
549 case 'before_create':
550 case 'before_update':
551 case 'before_validate':
552 case 'before_delete':
553 case 'after_create':
554 case 'after_update':
555 case 'after_validate':
556 case 'after_delete':
557 $fnc = array($this, $event);
558 $params = "events_params";
559 if (is_callable($fnc)) {
560 call_user_func_array($fnc, $$params);
562 break;
565 // }}}
567 // void runFilter(string $key, mixed &$value, mixed $past_value) {{{
569 * *Internal Method*
571 * This method check if the current document property has
572 * a filter method, if so, call it.
574 * If the filter returns false, throw an Exception.
576 * @return void
578 protected function runFilter($key, &$value, $past_value)
580 $filter = array($this, "{$key}_filter");
581 if (is_callable($filter)) {
582 $filter = call_user_func_array($filter, array(&$value, $past_value));
583 if ($filter===false) {
584 throw new ActiveMongo_FilterException("{$key} filter failed");
586 $this->$key = $value;
589 // }}}
591 // }}}
593 // void setCursor(MongoCursor $obj) {{{
595 * Set Cursor
597 * This method receive a MongoCursor and make
598 * it iterable.
600 * @param MongoCursor $obj
602 * @return void
604 final protected function setCursor(MongoCursor $obj)
606 $this->_cursor = $obj;
607 $this->setResult($obj->getNext());
609 // }}}
611 // void setResult(Array $obj) {{{
613 * Set Result
615 * This method takes an document and copy it
616 * as properties in this object.
618 * @param Array $obj
620 * @return void
622 final protected function setResult($obj)
624 /* Unsetting previous results, if any */
625 foreach (array_keys(get_document_vars($this, false)) as $key) {
626 unset($this->$key);
628 $this->_id = null;
630 /* Add our current resultset as our object's property */
631 foreach ((array)$obj as $key => $value) {
632 if ($key[0] == '$') {
633 continue;
635 $this->$key = $value;
638 /* Save our record */
639 $this->_current = $obj;
641 // }}}
643 // this find([$_id]) {{{
645 * Simple find.
647 * Really simple find, which uses this object properties
648 * for fast filtering
650 * @return object this
652 final function find($_id = null)
654 $vars = get_document_vars($this);
655 foreach ($vars as $key => $value) {
656 if (!$value) {
657 unset($vars[$key]);
659 $parent_class = __CLASS__;
660 if ($value InstanceOf $parent_class) {
661 $this->getColumnDeference($vars, $key, $value);
662 unset($vars[$key]); /* delete old value */
665 if ($_id != null) {
666 if (is_array($_id)) {
667 $vars['_id'] = array('$in' => $_id);
668 } else {
669 $vars['_id'] = $_id;
672 $res = $this->_getCollection()->find($vars);
673 $this->setCursor($res);
674 return $this;
676 // }}}
678 // void save(bool $async) {{{
680 * Save
682 * This method save the current document in MongoDB. If
683 * we're modifying a document, a update is performed, otherwise
684 * the document is inserted.
686 * On updates, special operations such as $set, $pushAll, $pullAll
687 * and $unset in order to perform efficient updates
689 * @param bool $async
691 * @return void
693 final function save($async=true)
695 $update = isset($this->_id) && $this->_id InstanceOf MongoID;
696 $conn = $this->_getCollection();
697 $document = $this->getCurrentDocument($update);
698 $object = get_document_vars($this);
699 if (count($document) == 0) {
700 return; /*nothing to do */
703 /* PRE-save hook */
704 $this->triggerEvent('before_'.($update ? 'update' : 'create'), array(&$document, $object));
706 if ($update) {
707 $conn->update(array('_id' => $this->_id), $document, array('safe' => $async));
708 if (isset($document['$set'])) {
709 foreach ($document['$set'] as $key => $value) {
710 $this->_current[$key] = $value;
711 $this->$key = $value;
714 if (isset($document['$unset'])) {
715 foreach ($document['$unset'] as $key => $value) {
716 unset($this->_current[$key]);
717 unset($this->$key);
720 } else {
721 $conn->insert($document, $async);
722 $this->setResult($document);
725 $this->triggerEvent('after_'.($update ? 'update' : 'create'), array($document, $object));
727 // }}}
729 // bool delete() {{{
731 * Delete the current document
733 * @return bool
735 final function delete()
737 if ($this->_cursor InstanceOf MongoCursor) {
738 $document = array('_id' => $this->_id);
739 $this->triggerEvent('before_delete', array($document));
740 $result = $this->_getCollection()->remove($document);
741 $this->triggerEvent('after_delete', array($document));
742 $this->setResult(array());
743 return $result;
744 } else {
745 $criteria = (array) $this->_query['query'];
747 /* remove */
748 $this->_getCollection()->remove($criteria);
750 /* reset object */
751 $this->reset();
753 return true;
755 return false;
757 // }}}
759 // Update {{{
761 * Multiple updates.
763 * This method perform multiple updates when a given
764 * criteria matchs (using where).
766 * By default the update is perform safely, but it can be
767 * changed.
769 * After the operation is done, the criteria is deleted.
771 * @param array $value Values to set
772 * @param bool $safe Whether or not peform the operation safely
774 * @return bool
777 function update(Array $value, $safe=true)
779 $this->_assertNotInQuery();
781 $criteria = (array) $this->_query['query'];
782 $options = array('multiple' => true, 'safe' => $safe);
784 /* update */
785 $col = $this->_getCollection();
786 $col->update($criteria, array('$set' => $value), $options);
788 /* reset object */
789 $this->reset();
791 return true;
793 // }}}
795 // void drop() {{{
797 * Delete the current colleciton and all its documents
799 * @return void
801 final static function drop()
803 $class = get_called_class();
804 if ($class == __CLASS__) {
805 return false;
807 $obj = new $class;
808 return $obj->_getCollection()->drop();
810 // }}}
812 // int count() {{{
814 * Return the number of documents in the actual request. If
815 * we're not in a request, it will return 0.
817 * @return int
819 final function count()
821 if ($this->valid()) {
822 return $this->_cursor->count();
824 return 0;
826 // }}}
828 // void setup() {{{
830 * This method should contain all the indexes, and shard keys
831 * needed by the current collection. This try to make
832 * installation on development environments easier.
834 function setup()
837 // }}}
839 // batchInsert {{{
840 /**
841 * Perform a batchInsert of objects.
843 * @param array $documents Arrays of documents to insert
844 * @param bool $safe True if a safe will be performed, this means data validation, and wait for MongoDB OK reply
845 * @param bool $on_error_continue If an error happen while validating an object, if it should continue or not
847 * @return bool
849 final public static function batchInsert(Array $documents, $safe=true, $on_error_continue=true)
851 if (__CLASS__ == get_called_class()) {
852 throw new ActiveMongo_Exception("Invalid batchInsert usage");
855 if ($safe) {
856 foreach ($documents as $id => $doc) {
857 $valid = false;
858 if (is_array($doc)) {
859 try {
860 self::triggerEvent('before_create', array(&$doc));
861 self::triggerEvent('before_validate', array(&$doc, $doc));
862 self::triggerEvent('before_validate_creation', array(&$doc, $doc));
863 $valid = true;
864 } catch (Exception $e) {}
866 if (!$valid) {
867 if (!$on_error_continue) {
868 throw new ActiveMongo_FilterException("Document $id is invalid");
870 unset($documents[$id]);
875 return self::_getCollection()->batchInsert($documents, array("safe" => $safe));
877 // }}}
879 // bool addIndex(array $columns, array $options) {{{
881 * addIndex
883 * Create an Index in the current collection.
885 * @param array $columns L ist of columns
886 * @param array $options Options
888 * @return bool
890 final function addIndex($columns, $options=array())
892 $default_options = array(
893 'background' => 1,
896 foreach ($default_options as $option => $value) {
897 if (!isset($options[$option])) {
898 $options[$option] = $value;
902 $collection = $this->_getCollection();
904 return $collection->ensureIndex($columns, $options);
906 // }}}
908 // string __toString() {{{
910 * To String
912 * If this object is treated as a string,
913 * it would return its ID.
915 * @return string
917 final function __toString()
919 return (string)$this->getID();
921 // }}}
923 // array sendCmd(array $cmd) {{{
925 * This method sends a command to the current
926 * database.
928 * @param array $cmd Current command
930 * @return array
932 final protected function sendCmd($cmd)
934 return $this->_getConnection()->command($cmd);
936 // }}}
938 // ITERATOR {{{
940 // void reset() {{{
942 * Reset our Object, delete the current cursor if any, and reset
943 * unsets the values.
945 * @return void
947 final function reset()
949 $this->_properties = null;
950 $this->_cursor = null;
951 $this->_cursor_ex = null;
952 $this->_query = null;
953 $this->_sort = null;
954 $this->_limit = 0;
955 $this->_skip = 0;
956 $this->setResult(array());
958 // }}}
960 // bool valid() {{{
962 * Valid
964 * Return if we're on an iteration and if it is still valid
966 * @return true
968 final function valid()
970 $valid = false;
971 if (!$this->_cursor_ex) {
972 if (!$this->_cursor InstanceOf MongoCursor) {
973 $this->doQuery();
975 $valid = $this->_cursor InstanceOf MongoCursor && $this->_cursor->valid();
976 } else {
977 switch ($this->_cursor_ex) {
978 case self::FIND_AND_MODIFY:
979 if ($this->_limit > $this->_findandmodify_cnt) {
980 $this->_execFindAndModify();
981 $valid = $this->_cursor_ex_value['ok'] == 1;
983 break;
984 default:
985 throw new ActiveMongo_Exception("Invalid _cursor_ex value");
989 return $valid;
991 // }}}
993 // bool next() {{{
995 * Move to the next document
997 * @return bool
999 final function next()
1001 if ($this->_cloned) {
1002 throw new MongoException("Cloned objects can't iterate");
1004 if (!$this->_cursor_ex) {
1005 return $this->_cursor->next();
1006 } else {
1007 switch ($this->_cursor_ex) {
1008 case self::FIND_AND_MODIFY:
1009 $this->_cursor_ex_value = NULL;
1010 break;
1011 default:
1012 throw new ActiveMongo_Exception("Invalid _cursor_ex value");
1016 // }}}
1018 // this current() {{{
1020 * Return the current object, and load the current document
1021 * as this object property
1023 * @return object
1025 final function current()
1027 if (!$this->_cursor_ex) {
1028 $this->setResult($this->_cursor->current());
1029 } else {
1030 switch ($this->_cursor_ex) {
1031 case self::FIND_AND_MODIFY:
1032 if (count($this->_cursor_ex_value) == 0) {
1033 $this->_execFindAndModify();
1035 $this->setResult($this->_cursor_ex_value['value']);
1036 break;
1037 default:
1038 throw new ActiveMongo_Exception("Invalid _cursor_ex value");
1041 return $this;
1043 // }}}
1045 // bool rewind() {{{
1047 * Go to the first document
1049 final function rewind()
1051 if (!$this->_cursor_ex) {
1052 /* rely on MongoDB cursor */
1053 if (!$this->_cursor InstanceOf MongoCursor) {
1054 $this->doQuery();
1056 return $this->_cursor->rewind();
1057 } else {
1058 switch ($this->_cursor_ex) {
1059 case self::FIND_AND_MODIFY:
1060 $this->_findandmodify_cnt = 0;
1061 break;
1062 default:
1063 throw new ActiveMongo_Exception("Invalid _cursor_ex value");
1067 // }}}
1069 // }}}
1071 // ARRAY ACCESS {{{
1072 final function offsetExists($offset)
1074 return isset($this->$offset);
1077 final function offsetGet($offset)
1079 return $this->$offset;
1082 final function offsetSet($offset, $value)
1084 $this->$offset = $value;
1087 final function offsetUnset($offset)
1089 unset($this->$offset);
1091 // }}}
1093 // REFERENCES {{{
1095 // array getReference() {{{
1097 * ActiveMongo extended the Mongo references, adding
1098 * the concept of 'dynamic' requests, saving in the database
1099 * the current query with its options (sort, limit, etc).
1101 * This is useful to associate a document with a given
1102 * request. To undestand this better please see the 'reference'
1103 * example.
1105 * @return array
1107 final function getReference($dynamic=false)
1109 if (!$this->getID() && !$dynamic) {
1110 return null;
1113 $document = array(
1114 '$ref' => $this->getCollectionName(),
1115 '$id' => $this->getID(),
1116 '$db' => $this->getDatabaseName(),
1117 'class' => get_class($this),
1120 if ($dynamic && $this->_cursor InstanceOf MongoCursor) {
1121 $cursor = $this->_cursor;
1122 if (!is_callable(array($cursor, "Info"))) {
1123 throw new Exception("Please upgrade your PECL/Mongo module to use this feature");
1125 $document['dynamic'] = array();
1126 $query = $cursor->Info();
1127 foreach ($query as $type => $value) {
1128 $document['dynamic'][$type] = $value;
1131 return $document;
1133 // }}}
1135 // void getDocumentReferences($document, &$refs) {{{
1137 * Get Current References
1139 * Inspect the current document trying to get any references,
1140 * if any.
1142 * @param array $document Current document
1143 * @param array &$refs References found in the document.
1144 * @param array $parent_key Parent key
1146 * @return void
1148 final protected function getDocumentReferences($document, &$refs, $parent_key=null)
1150 foreach ($document as $key => $value) {
1151 if (is_array($value)) {
1152 if (MongoDBRef::isRef($value)) {
1153 $pkey = $parent_key;
1154 $pkey[] = $key;
1155 $refs[] = array('ref' => $value, 'key' => $pkey);
1156 } else {
1157 $parent_key[] = $key;
1158 $this->getDocumentReferences($value, $refs, $parent_key);
1163 // }}}
1165 // object _deferencingCreateObject(string $class) {{{
1167 * Called at deferencig time
1169 * Check if the given string is a class, and it is a sub class
1170 * of ActiveMongo, if it is instance and return the object.
1172 * @param string $class
1174 * @return object
1176 private function _deferencingCreateObject($class)
1178 if (!is_subclass_of($class, __CLASS__)) {
1179 throw new MongoException("Fatal Error, imposible to create ActiveMongo object of {$class}");
1181 return new $class;
1183 // }}}
1185 // void _deferencingRestoreProperty(array &$document, array $keys, mixed $req) {{{
1187 * Called at deferencig time
1189 * This method iterates $document until it could match $keys path, and
1190 * replace its value by $req.
1192 * @param array &$document Document to replace
1193 * @param array $keys Path of property to change
1194 * @param mixed $req Value to replace.
1196 * @return void
1198 private function _deferencingRestoreProperty(&$document, $keys, $req)
1200 $obj = & $document;
1202 /* find the $req proper spot */
1203 foreach ($keys as $key) {
1204 $obj = & $obj[$key];
1207 $obj = $req;
1209 /* Delete reference variable */
1210 unset($obj);
1212 // }}}
1214 // object _deferencingQuery($request) {{{
1216 * Called at deferencig time
1218 * This method takes a dynamic reference and request
1219 * it to MongoDB.
1221 * @param array $request Dynamic reference
1223 * @return this
1225 private function _deferencingQuery($request)
1227 $collection = $this->_getCollection();
1228 $cursor = $collection->find($request['query'], $request['fields']);
1229 if ($request['limit'] > 0) {
1230 $cursor->limit($request['limit']);
1232 if ($request['skip'] > 0) {
1233 $cursor->limit($request['limit']);
1236 $this->setCursor($cursor);
1238 return $this;
1240 // }}}
1242 // void doDeferencing() {{{
1244 * Perform a deferencing in the current document, if there is
1245 * any reference.
1247 * ActiveMongo will do its best to group references queries as much
1248 * as possible, in order to perform as less request as possible.
1250 * ActiveMongo doesn't rely on MongoDB references, but it can support
1251 * it, but it is prefered to use our referencing.
1253 * @experimental
1255 final function doDeferencing($refs=array())
1257 /* Get current document */
1258 $document = get_document_vars($this);
1260 if (count($refs)==0) {
1261 /* Inspect the whole document */
1262 $this->getDocumentReferences($document, $refs);
1265 $db = $this->_getConnection();
1267 /* Gather information about ActiveMongo Objects
1268 * that we need to create
1270 $classes = array();
1271 foreach ($refs as $ref) {
1272 if (!isset($ref['ref']['class'])) {
1274 /* Support MongoDBRef, we do our best to be compatible {{{ */
1275 /* MongoDB 'normal' reference */
1277 $obj = MongoDBRef::get($db, $ref['ref']);
1279 /* Offset the current document to the right spot */
1280 /* Very inefficient, never use it, instead use ActiveMongo References */
1282 $this->_deferencingRestoreProperty($document, $ref['key'], clone $req);
1284 /* Dirty hack, override our current document
1285 * property with the value itself, in order to
1286 * avoid replace a MongoDB reference by its content
1288 $this->_deferencingRestoreProperty($this->_current, $ref['key'], clone $req);
1290 /* }}} */
1292 } else {
1294 if (isset($ref['ref']['dynamic'])) {
1295 /* ActiveMongo Dynamic Reference */
1297 /* Create ActiveMongo object */
1298 $req = $this->_deferencingCreateObject($ref['ref']['class']);
1300 /* Restore saved query */
1301 $req->_deferencingQuery($ref['ref']['dynamic']);
1303 $results = array();
1305 /* Add the result set */
1306 foreach ($req as $result) {
1307 $results[] = clone $result;
1310 /* add information about the current reference */
1311 foreach ($ref['ref'] as $key => $value) {
1312 $results[$key] = $value;
1315 $this->_deferencingRestoreProperty($document, $ref['key'], $results);
1317 } else {
1318 /* ActiveMongo Reference FTW! */
1319 $classes[$ref['ref']['class']][] = $ref;
1324 /* {{{ Create needed objects to query MongoDB and replace
1325 * our references by its objects documents.
1327 foreach ($classes as $class => $refs) {
1328 $req = $this->_deferencingCreateObject($class);
1330 /* Load list of IDs */
1331 $ids = array();
1332 foreach ($refs as $ref) {
1333 $ids[] = $ref['ref']['$id'];
1336 /* Search to MongoDB once for all IDs found */
1337 $req->find($ids);
1339 if ($req->count() != count($refs)) {
1340 $total = $req->count();
1341 $expected = count($refs);
1342 throw new MongoException("Dereferencing error, MongoDB replied {$total} objects, we expected {$expected}");
1345 /* Replace our references by its objects */
1346 foreach ($refs as $ref) {
1347 $id = $ref['ref']['$id'];
1348 $place = $ref['key'];
1349 $req->rewind();
1350 while ($req->getID() != $id && $req->next());
1352 assert($req->getID() == $id);
1354 $this->_deferencingRestoreProperty($document, $place, clone $req);
1356 unset($obj);
1359 /* Release request, remember we
1360 * safely cloned it,
1362 unset($req);
1364 // }}}
1366 /* Replace the current document by the new deferenced objects */
1367 foreach ($document as $key => $value) {
1368 $this->$key = $value;
1371 // }}}
1373 // void getColumnDeference(&$document, $propety, ActiveMongo Obj) {{{
1375 * Prepare a "selector" document to search treaing the property
1376 * as a reference to the given ActiveMongo object.
1379 final function getColumnDeference(&$document, $property, ActiveMongo $obj)
1381 $document["{$property}.\$id"] = $obj->getID();
1383 // }}}
1385 // void findReferences(&$document) {{{
1387 * Check if in the current document to insert or update
1388 * exists any references to other ActiveMongo Objects.
1390 * @return void
1392 final function findReferences(&$document)
1394 if (!is_array($document)) {
1395 return;
1397 foreach($document as &$value) {
1398 $parent_class = __CLASS__;
1399 if (is_array($value)) {
1400 if (MongoDBRef::isRef($value)) {
1401 /* If the property we're inspecting is a reference,
1402 * we need to remove the values, restoring the valid
1403 * Reference.
1405 $arr = array(
1406 '$ref'=>1, '$id'=>1, '$db'=>1, 'class'=>1, 'dynamic'=>1
1408 foreach (array_keys($value) as $key) {
1409 if (!isset($arr[$key])) {
1410 unset($value[$key]);
1413 } else {
1414 $this->findReferences($value);
1416 } else if ($value InstanceOf $parent_class) {
1417 $value = $value->getReference();
1420 /* trick: delete last var. reference */
1421 unset($value);
1423 // }}}
1425 // void __clone() {{{
1426 /**
1427 * Cloned objects are rarely used, but ActiveMongo
1428 * uses it to create different objects per everyrecord,
1429 * which is used at deferencing. Therefore cloned object
1430 * do not contains the recordset, just the actual document,
1431 * so iterations are not allowed.
1434 final function __clone()
1436 unset($this->_cursor);
1437 $this->_cloned = true;
1439 // }}}
1441 // }}}
1443 // GET DOCUMENT ID {{{
1445 // getID() {{{
1447 * Return the current document ID. If there is
1448 * no document it would return false.
1450 * @return object|false
1452 final public function getID()
1454 if ($this->_id instanceof MongoID) {
1455 return $this->_id;
1457 return false;
1459 // }}}
1461 // string key() {{{
1463 * Return the current key
1465 * @return string
1467 final function key()
1469 return $this->getID();
1471 // }}}
1473 // }}}
1475 // Fancy (and silly) query abstraction {{{
1477 // _assertNotInQuery() {{{
1479 * Check if we can modify the query or not. We cannot modify
1480 * the query if we're iterating over and oldest query, in this case the
1481 * object must be reset.
1483 * @return void
1485 final private function _assertNotInQuery()
1487 if ($this->_cursor InstanceOf MongoCursor || $this->_cursor_ex != NULL) {
1488 throw new ActiveMongo_Exception("You cannot modify the query, please reset the object");
1491 // }}}
1493 // doQuery() {{{
1495 * Build the current request and send it to MongoDB.
1497 * @return this
1499 final function doQuery()
1501 if ($this->_cursor_ex) {
1502 switch ($this->_cursor_ex) {
1503 case self::FIND_AND_MODIFY:
1504 $this->_cursor_ex_value = NULL;
1505 return;
1506 default:
1507 throw new ActiveMongo_Exception("Invalid _cursor_ex value");
1510 $this->_assertNotInQuery();
1512 $col = $this->_getCollection();
1513 if (count($this->_properties) > 0) {
1514 $cursor = $col->find((array)$this->_query['query'], $this->_properties);
1515 } else {
1516 $cursor = $col->find((array)$this->_query['query']);
1518 if (is_array($this->_sort)) {
1519 $cursor->sort($this->_sort);
1521 if ($this->_limit > 0) {
1522 $cursor->limit($this->_limit);
1524 if ($this->_skip > 0) {
1525 $cursor->skip($this->_skip);
1528 /* Our cursor must be sent to ActiveMongo */
1529 $this->setCursor($cursor);
1531 return $this;
1533 // }}}
1535 // properties($props) {{{
1537 * Select 'properties' or 'columns' to be included in the document,
1538 * by default all properties are included.
1540 * @param array $props
1542 * @return this
1544 final function properties($props)
1546 $this->_assertNotInQuery();
1548 if (!is_array($props) && !is_string($props)) {
1549 return false;
1552 if (is_string($props)) {
1553 $props = explode(",", $props);
1556 foreach ($props as $id => $name) {
1557 $props[trim($name)] = 1;
1558 unset($props[$id]);
1561 $this->_properties = $props;
1563 return $this;
1566 final function columns($properties)
1568 return $this->properties($properties);
1570 // }}}
1572 // where($property, $value) {{{
1574 * Where abstraction.
1577 final function where($property_str, $value=null)
1579 $this->_assertNotInQuery();
1581 if (is_array($property_str)) {
1582 if ($value != null) {
1583 throw new ActiveMongo_Expception("Invalid parameters");
1585 foreach ($property_str as $property => $value) {
1586 if (is_numeric($property)) {
1587 $property = $value;
1588 $value = 0;
1590 $this->where($property, $value);
1592 return $this;
1595 $column = explode(" ", trim($property_str));
1596 if (count($column) != 1 && count($column) != 2) {
1597 throw new ActiveMongo_Exception("Failed while parsing '{$property_str}'");
1598 } else if (count($column) == 2) {
1600 $exp_scalar = true;
1601 switch (strtolower($column[1])) {
1602 case '>':
1603 case '$gt':
1604 $op = '$gt';
1605 break;
1607 case '>=':
1608 case '$gte':
1609 $op = '$gte';
1610 break;
1612 case '<':
1613 case '$lt':
1614 $op = '$lt';
1615 break;
1617 case '<=':
1618 case '$lte':
1619 $op = '$lte';
1620 break;
1622 case '==':
1623 case '$eq':
1624 case '=':
1625 if (is_array($value)) {
1626 $op = '$all';
1627 $exp_scalar = false;
1628 } else {
1629 $op = '$eq';
1631 break;
1633 case '!=':
1634 case '<>':
1635 case '$ne':
1636 if (is_array($value)) {
1637 $op = '$nin';
1638 $exp_scalar = false;
1639 } else {
1640 $op = '$ne';
1642 break;
1644 case '%':
1645 case 'mod':
1646 case '$mod':
1647 $op = '$mod';
1648 break;
1650 case 'exists':
1651 case '$exists':
1652 if ($value === NULL) {
1653 $value = true;
1655 $op = '$exists';
1656 break;
1658 /* regexp */
1659 case 'regexp':
1660 case 'regex':
1661 $value = new MongoRegex($value);
1662 $op = NULL;
1663 break;
1665 /* arrays */
1666 case 'in':
1667 case '$in':
1668 $exp_scalar = false;
1669 $op = '$in';
1670 break;
1672 case '$nin':
1673 case 'nin':
1674 $exp_scalar = false;
1675 $op = '$nin';
1676 break;
1679 /* geo operations */
1680 case 'near':
1681 case '$near':
1682 $op = '$near';
1683 $exp_scalar = false;
1684 break;
1686 default:
1687 throw new ActiveMongo_Exception("Failed to parse '{$column[1]}'");
1690 if ($exp_scalar && is_array($value)) {
1691 throw new ActiveMongo_Exception("Cannot use comparing operations with Array");
1692 } else if (!$exp_scalar && !is_array($value)) {
1693 throw new ActiveMongo_Exception("The operation {$column[1]} expected an Array");
1696 if ($op) {
1697 $value = array($op => $value);
1699 } else if (is_array($value)) {
1700 $value = array('$in' => $value);
1703 $spot = & $this->_query['query'][$column[0]];
1704 if (is_array($value)) {
1705 $spot[key($value)] = current($value);
1706 } else {
1707 /* simulate AND among same properties if
1708 * multiple values is passed for same property
1710 if (isset($spot)) {
1711 if (is_array($spot)) {
1712 $spot['$all'][] = $value;
1713 } else {
1714 $spot = array('$all' => array($spot, $value));
1716 } else {
1717 $spot = $value;
1721 return $this;
1723 // }}}
1725 // sort($sort_str) {{{
1727 * Abstract the documents sorting.
1729 * @param string $sort_str List of properties to use as sorting
1731 * @return this
1733 final function sort($sort_str)
1735 $this->_assertNotInQuery();
1737 $this->_sort = array();
1738 foreach ((array)explode(",", $sort_str) as $sort_part_str) {
1739 $sort_part = explode(" ", trim($sort_part_str), 2);
1740 switch(count($sort_part)) {
1741 case 1:
1742 $sort_part[1] = 'ASC';
1743 break;
1744 case 2:
1745 break;
1746 default:
1747 throw new ActiveMongo_Exception("Don't know how to parse {$sort_part_str}");
1750 switch (strtoupper($sort_part[1])) {
1751 case 'ASC':
1752 $sort_part[1] = 1;
1753 break;
1754 case 'DESC':
1755 $sort_part[1] = -1;
1756 break;
1757 default:
1758 throw new ActiveMongo_Exception("Invalid sorting direction `{$sort_part[1]}`");
1760 $this->_sort[ $sort_part[0] ] = $sort_part[1];
1763 return $this;
1765 // }}}
1767 // limit($limit, $skip) {{{
1769 * Abstract the limitation and pagination of documents.
1771 * @param int $limit Number of max. documents to retrieve
1772 * @param int $skip Number of documents to skip
1774 * @return this
1776 final function limit($limit=0, $skip=0)
1778 $this->_assertNotInQuery();
1780 if ($limit < 0 || $skip < 0) {
1781 return false;
1783 $this->_limit = $limit;
1784 $this->_skip = $skip;
1786 return $this;
1788 // }}}
1790 // FindAndModify(Array $document) {{{
1792 * findAndModify
1796 final function findAndModify($document)
1798 $this->_assertNotInQuery();
1800 if (count($document) === 0) {
1801 throw new ActiveMongo_Exception("Empty \$document is not allowed");
1804 $this->_cursor_ex = self::FIND_AND_MODIFY;
1805 $this->_findandmodify = $document;
1807 return $this;
1810 private function _execFindAndModify()
1812 $query = (array)$this->_query['query'];
1814 $query = array(
1815 "findandmodify" => $this->getCollectionName(),
1816 "query" => $query,
1817 "update" => array('$set' => $this->_findandmodify),
1818 "new" => true,
1820 $this->_cursor_ex_value = $this->sendCMD($query);
1821 if (isset($this->_query['sort'])) {
1822 $query["sort"] = $this->_query['sort'];
1825 $this->_findandmodify_cnt++;
1827 // }}}
1829 // }}}
1833 require_once dirname(__FILE__)."/Validators.php";
1834 require_once dirname(__FILE__)."/Exceptions.php";
1837 * Local variables:
1838 * tab-width: 4
1839 * c-basic-offset: 4
1840 * End:
1841 * vim600: sw=4 ts=4 fdm=marker
1842 * vim<600: sw=4 ts=4