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 +---------------------------------------------------------------------------------+
39 // Class FilterException {{{
43 * This is Exception is thrown if any validation
44 * fails when save() is called.
47 final class FilterException
extends Exception
52 // array get_object_vars_ex(stdobj $obj) {{{
54 * Simple hack to avoid get private and protected variables
60 function get_object_vars_ex($obj)
62 return get_object_vars($obj);
69 * Simple ActiveRecord pattern built on top of MongoDB. This class
70 * aims to provide easy iteration, data validation before update,
71 * and efficient update.
73 * @author César D. Rodas <crodas@php.net>
74 * @license PHP License
75 * @package ActiveMongo
79 abstract class ActiveMongo
implements Iterator
84 * Current databases objects
90 * Current collections objects
94 private static $_collections;
96 * Current connection to MongoDB
98 * @type MongoConnection
100 private static $_conn;
112 private static $_host;
118 private $_current = array();
124 private $_cursor = null;
126 * Current document ID
132 private $_cloned = false;
135 // string getCollectionName() {{{
137 * Get Collection Name, by default the class name,
138 * but you it can be override at the class itself to give
141 * @return string Collection Name
143 protected function getCollectionName()
145 return strtolower(get_class($this));
149 // string getDatabaseName() {{{
151 * Get Database Name, by default it is used
152 * the db name set by ActiveMong::connect()
154 * @return string DB Name
156 protected function getDatabaseName()
158 if (is_null(self
::$_db)) {
159 throw new MongoException("There is no information about the default DB name");
165 // void install() {{{
169 * This static method iterate over the classes lists,
170 * and execute the setup() method on every ActiveMongo
171 * subclass. You should do this just once.
174 final public static function install()
176 $classes = array_reverse(get_declared_classes());
177 foreach ($classes as $class)
179 if ($class == __CLASS__
) {
182 if (is_subclass_of($class, __CLASS__
)) {
190 // void connection($db, $host) {{{
194 * This method setup parameters to connect to a MongoDB
195 * database. The connection is done when it is needed.
197 * @param string $db Database name
198 * @param string $host Host to connect
202 final public static function connect($db, $host='localhost')
204 self
::$_host = $host;
209 // MongoConnection _getConnection() {{{
213 * Get a valid database connection
215 * @return MongoConnection
217 final protected function _getConnection()
219 if (is_null(self
::$_conn)) {
220 if (is_null(self
::$_host)) {
221 self
::$_host = 'localhost';
223 self
::$_conn = new Mongo(self
::$_host);
225 $dbname = $this->getDatabaseName();
226 if (!isSet(self
::$_dbs[$dbname])) {
227 self
::$_dbs[$dbname] = self
::$_conn->selectDB($dbname);
229 return self
::$_dbs[$dbname];
233 // MongoCollection _getCollection() {{{
237 * Get a collection connection.
239 * @return MongoCollection
241 final protected function _getCollection()
243 $colName = $this->getCollectionName();
244 if (!isset(self
::$_collections[$colName])) {
245 self
::$_collections[$colName] = self
::_getConnection()->selectCollection($colName);
247 return self
::$_collections[$colName];
251 // bool getCurrentSubDocument(array &$document, string $parent_key, array $values, array $past_values) {{{
253 * Generate Sub-document
255 * This method build the difference between the current sub-document,
256 * and the origin one. If there is no difference, it would do nothing,
257 * otherwise it would build a document containing the differences.
259 * @param array &$document Document target
260 * @param string $parent_key Parent key name
261 * @param array $values Current values
262 * @param array $past_values Original values
266 final function getCurrentSubDocument(&$document, $parent_key, Array $values, Array $past_values)
269 * The current property is a embedded-document,
270 * now we're looking for differences with the
271 * previous value (because we're on an update).
273 * It behaves exactly as getCurrentDocument,
274 * but this is simples (it doesn't support
277 foreach ($values as $key => $value) {
278 $super_key = "{$parent_key}.{$key}";
279 if (is_array($value)) {
281 * Inner document detected
283 if (!isset($past_values[$key]) ||
!is_array($past_values[$key])) {
285 * We're lucky, it is a new sub-document,
288 $document['$set'][$super_key] = $value;
291 * This is a document like this, we need
292 * to find out the differences to avoid
295 if (!$this->getCurrentSubDocument($document, $super_key, $value, $past_values[$key])) {
300 } else if (!isset($past_values[$key]) ||
$past_values[$key] != $value) {
301 $document['$set'][$super_key] = $value;
305 foreach (array_diff(array_keys($past_values), array_keys($values)) as $key) {
306 $super_key = "{$parent_key}.{$key}";
307 $document['$unset'][$super_key] = 1;
314 // array getCurrentDocument(bool $update) {{{
316 * Get Current Document
318 * Based on this object properties a new document (Array)
319 * is returned. If we're modifying an document, just the modified
320 * properties are included in this document, which uses $set,
321 * $unset, $pushAll and $pullAll.
324 * @param bool $update
328 final protected function getCurrentDocument($update=false, $current=false)
331 $object = get_object_vars_ex($this);
334 $current = (array)$this->_current
;
337 $this->findReferences($object);
339 foreach ($object as $key => $value) {
344 if (is_array($value) && isset($current[$key])) {
346 * If the Field to update is an array, it has a different
347 * behaviour other than $set and $unset. Fist, we need
348 * need to check if it is an array or document, because
349 * they can't be mixed.
352 if (!is_array($current[$key])) {
354 * We're lucky, the field wasn't
355 * an array previously.
357 $this->_call_filter($key, $value, $current[$key]);
358 $document['$set'][$key] = $value;
362 if (!$this->getCurrentSubDocument($document, $key, $value, $current[$key])) {
363 throw new Exception("{$key}: Array and documents are not compatible");
365 } else if(!isset($current[$key]) ||
$value !== $current[$key]) {
367 * It is 'linear' field that has changed, or
370 $past_value = isset($current[$key]) ?
$current[$key] : null;
371 $this->_call_filter($key, $value, $past_value);
372 $document['$set'][$key] = $value;
376 * It is a document insertation, so we
377 * create the document.
379 $this->_call_filter($key, $value, null);
380 $document[$key] = $value;
384 /* Updated behaves in a diff. way */
386 foreach (array_diff(array_keys($this->_current
), array_keys($object)) as $property) {
387 if ($property == '_id') {
390 $document['$unset'][$property] = 1;
394 if (count($document) == 0) {
401 // void _call_filter(string $key, mixed &$value, mixed $past_value) {{{
405 * This method check if the current document property has
406 * a filter method, if so, call it.
408 * If the filter returns false, throw an Exception.
412 private function _call_filter($key, &$value, $past_value)
414 $filter = array($this, "{$key}_filter");
415 if (is_callable($filter)) {
416 $filter = call_user_func_array($filter, array(&$value, $past_value));
417 if ($filter===false) {
418 throw new FilterException("{$key} filter failed");
420 $this->$key = $value;
425 // void setCursor(MongoCursor $obj) {{{
429 * This method receive a MongoCursor and make
432 * @param MongoCursor $obj
436 final protected function setCursor(MongoCursor
$obj)
438 $this->_cursor
= $obj;
439 $this->setResult($obj->getNext());
445 * Reset our Object, delete the current cursor if any, and reset
450 final function reset()
453 $this->_cursor
= null;
454 $this->setResult(array());
458 // void setResult(Array $obj) {{{
462 * This method takes an document and copy it
463 * as properties in this object.
469 final protected function setResult($obj)
471 /* Unsetting previous results, if any */
472 foreach (array_keys((array)$this->_current
) as $key) {
476 /* Add our current resultset as our object's property */
477 foreach ((array)$obj as $key => $value) {
478 if ($key[0] == '$') {
481 $this->$key = $value;
484 /* Save our record */
485 $this->_current
= $obj;
489 // this find([$_id]) {{{
493 * Really simple find, which uses this object properties
496 * @return object this
498 final function find($_id = null)
500 $vars = get_object_vars_ex($this);
501 foreach ($vars as $key => $value) {
505 if ($value InstanceOf ActiveMongo
) {
506 $this->getColumnDeference($vars, $key, $value);
507 unset($vars[$key]); /* delete old value */
511 if (is_array($_id)) {
512 $vars['_id'] = array('$in' => $_id);
517 $res = $this->_getCollection()->find($vars);
518 $this->setCursor($res);
523 // void save(bool $async) {{{
527 * This method save the current document in MongoDB. If
528 * we're modifying a document, a update is performed, otherwise
529 * the document is inserted.
531 * On updates, special operations such as $set, $pushAll, $pullAll
532 * and $unset in order to perform efficient updates
538 final function save($async=true)
540 $update = isset($this->_id
) && $this->_id
InstanceOf MongoID
;
541 $conn = $this->_getCollection();
542 $obj = $this->getCurrentDocument($update);
543 if (count($obj) == 0) {
544 return; /*nothing to do */
548 $this->pre_save($update ?
'update' : 'create', $obj);
550 $conn->update(array('_id' => $this->_id
), $obj);
551 foreach ($obj as $key => $value) {
552 if ($key[0] == '$') {
555 $this->_current
[$key] = $value;
559 $conn->insert($obj, $async);
560 $this->_id
= $obj['_id'];
561 $this->_current
= $obj;
569 * Delete the current document
573 final function delete()
575 if ($this->valid()) {
576 return $this->_getCollection()->remove(array('_id' => $this->_id
));
584 * Delete the current colleciton and all its documents
588 final static function drop()
590 $class = get_called_class();
591 if ($class == __CLASS__
) {
595 return $obj->_getCollection()->drop();
601 * Return the number of documents in the actual request. If
602 * we're not in a request, it will return 0.
606 final function count()
608 if ($this->valid()) {
609 return $this->_cursor
->count();
621 * Return if we're on an iteration and if it is still valid
625 final function valid()
627 return $this->_cursor
InstanceOf MongoCursor
&& $this->_cursor
->valid();
633 * Move to the next document
637 final function next()
639 if ($this->_cloned
) {
640 throw new MongoException("Cloned objects can't iterate");
642 return $this->_cursor
->next();
646 // this current() {{{
648 * Return the current object, and load the current document
649 * as this object property
653 final function current()
655 $this->setResult($this->_cursor
->current());
662 * Go to the first document
664 final function rewind()
666 return $this->_cursor
->rewind();
674 // array getReference() {{{
676 * ActiveMongo extended the Mongo references, adding
677 * the concept of 'dynamic' requests, saving in the database
678 * the current query with its options (sort, limit, etc).
680 * This is useful to associate a document with a given
681 * request. To undestand this better please see the 'reference'
686 final function getReference($dynamic=false)
688 if (!$this->getID()) {
693 '$ref' => $this->getCollectionName(),
694 '$id' => $this->getID(),
695 '$db' => $this->getDatabaseName(),
696 'class' => get_class($this),
699 if ($dynamic && $this->valid()) {
700 $cursor = $this->_cursor
;
701 if (!is_callable(array($cursor, "getQuery"))) {
702 throw new Exception("Please upgrade your PECL/Mongo module to use this feature");
704 $document['dynamic'] = array();
705 $query = $cursor->getQuery();
706 foreach ($query as $type => $value) {
707 $document['dynamic'][$type] = $value;
714 // void getCurrentReferences($document, &$refs) {{{
716 * Get Current References
718 * Inspect the current document trying to get any references,
721 * @param array $document Current document
722 * @param array &$refs References found in the document.
723 * @param array $parent_key Parent key
727 final protected function getCurrentReferences($document, &$refs, $parent_key=null)
729 foreach ($document as $key => $value) {
730 if (is_array($value)) {
731 if (MongoDBRef
::isRef($value)) {
734 $refs[] = array('ref' => $value, 'key' => $pkey);
736 $parent_key[] = $key;
737 $this->getCurrentReferences($value, $refs, $parent_key);
744 // void doDeferencing() {{{
746 * Perform a deferencing in the current document, if there is
749 * ActiveMongo will do its best to group references queries as much
750 * as possible, in order to perform as less request as possible.
752 * ActiveMongo doesn't rely on MongoDB references, but it can support
753 * it, but it is prefered to use our referencing.
757 final function doDeferencing($refs=array())
759 /* Get current document */
760 $document = get_object_vars_ex($this);
762 if (count($refs)==0) {
763 /* Inspect the whole document */
764 $this->getCurrentReferences($document, $refs);
767 $db = $this->_getConnection();
769 /* Gather information about ActiveMongo Objects
770 * that we need to create
773 foreach ($refs as $ref) {
774 if (!isset($ref['ref']['class'])) {
776 /* Support MongoDBRef {{{ */
777 /* MongoDB 'normal' reference */
778 /* Offset the current document to the right spot */
779 /* Very inefficient, never use it, instead use ActiveMongo References */
781 foreach ($ref['key'] as $key) {
784 $obj = MongoDBRef
::get($db, $ref['ref']);
786 /* Dirty hack, override our current document
787 * property with the value itself, in order to
788 * avoid replace a MongoDB reference by its content
790 $_obj = & $this->_current
;
791 foreach ($ref['key'] as $key) {
792 $_obj = & $_obj[$key];
794 $_obj = MongoDBRef
::get($db, $ref['ref']);
797 /* Delete reference variable */
802 /* ActiveMongo Reference FTW! */
803 $classes[$ref['ref']['class']][] = $ref;
807 /* {{{ Create needed objects to query MongoDB and replace
808 * our references by its objects documents.
811 foreach ($classes as $class => $refs) {
814 /* Load list of IDs */
816 foreach ($refs as $ref) {
817 $ids[] = $ref['ref']['$id'];
820 /* Search to MongoDB once for all IDs found */
823 if ($req->count() != count($refs)) {
824 $total = $req->count();
825 $expected = count($refs);
826 throw new MongoException("Dereferencing error, MongoDB replied {$total} objects, we expected {$expected}");
829 /* Replace our references by its objects */
830 foreach ($refs as $ref) {
831 $id = $ref['ref']['$id'];
832 $place = $ref['key'];
834 while ($req->getID() != $id && $req->next());
836 assert($req->getID() == $id);
839 foreach ($ref['key'] as $key) {
844 /* Delete reference variable */
848 /* Release request, remember we
855 /* Replace the current document by the new deferenced objects */
856 foreach ($document as $key => $value) {
857 $this->$key = $value;
862 // void getColumnDeference(&$document, $propety, ActiveMongo Obj) {{{
864 * Prepare a "selector" document to search treaing the property
865 * as a reference to the given ActiveMongo object.
868 final function getColumnDeference(&$document, $property, ActiveMongo
$obj)
870 $document["{$property}.\$id"] = $obj->getID();
874 // void findReferences(&$document) {{{
876 * Check if in the current document to insert or update
877 * exists any references to other ActiveMongo Objects.
881 final function findReferences(&$document)
883 if (!is_array($document)) {
886 foreach($document as &$value) {
887 if ($value InstanceOf ActiveMongo
) {
888 $value = $value->getReference();
889 } else if (is_array($value)) {
890 $this->findReferences($value);
893 /* trick: delete last var. reference */
898 // void __clone() {{{
900 * Cloned objects are rarely used, but ActiveMongo
901 * uses it to create different objects per everyrecord,
902 * which is used at deferencing. Therefore cloned object
903 * do not contains the recordset, just the actual document,
904 * so iterations are not allowed.
907 final function __clone()
909 unset($this->_cursor
);
910 $this->_cloned
= true;
916 // GET DOCUMENT ID {{{
920 * Return the current document ID. If there is
921 * no document it would return false.
923 * @return object|false
925 final public function getID()
927 if ($this->_id
instanceof MongoID
) {
936 * Return the current key
942 return $this->getID();
950 // void pre_save($action, & $document) {{{
954 * This method is fired just before an insert or updated. The document
955 * is passed by reference, so it can be modified. Also if for instance
956 * one property is missing an Exception could be thrown to avoid
960 * @param string $action Update or Create
961 * @param array &$document Document that will be sent to MongoDB.
965 protected function pre_save($action, Array &$document)
970 // void on_save() {{{
974 * This method is fired right after an insert is performed.
978 protected function on_save()
983 // void on_update() {{{
987 * This method is fired right after an update is performed.
991 protected function on_update()
996 // void on_iterate() {{{
1000 * This method is fired right after a new document is loaded
1001 * from the recorset, it could be useful to load references to other
1006 protected function on_iterate()
1015 * This method should contain all the indexes, and shard keys
1016 * needed by the current collection. This try to make
1017 * installation on development environments easier.
1024 // bool addIndex(array $columns, array $options) {{{
1028 * Create an Index in the current collection.
1030 * @param array $columns L ist of columns
1031 * @param array $options Options
1035 final function addIndex($columns, $options=array())
1037 $default_options = array(
1041 foreach ($default_options as $option => $value) {
1042 if (!isset($options[$option])) {
1043 $options[$option] = $value;
1047 $collection = $this->_getCollection();
1049 return $collection->ensureIndex($columns, $options);
1053 // string __toString() {{{
1057 * If this object is treated as a string,
1058 * it would return its ID.
1062 final function __toString()
1064 return (string)$this->getID();
1068 // array sendCmd(array $cmd) {{{
1070 * This method sends a command to the current
1073 * @param array $cmd Current command
1077 final protected function sendCmd($cmd)
1079 return $this->_getConnection()->command($cmd);
1090 * vim600: sw=4 ts=4 fdm=marker
1091 * vim<600: sw=4 ts=4