- Changed the Project License, adopted the BSD License.
[activemongo.git] / ActiveMongo.php
blob9755f81dd80db0e9ea9c637d19fbce01465fae79
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 +---------------------------------------------------------------------------------+
39 // Class FilterException {{{
40 /**
41 * FilterException
43 * This is Exception is thrown if any validation
44 * fails when save() is called.
47 final class FilterException extends Exception
50 // }}}
52 // array get_object_vars_ex(stdobj $obj) {{{
53 /**
54 * Simple hack to avoid get private and protected variables
56 * @param obj
58 * @return array
60 function get_object_vars_ex($obj)
62 return get_object_vars($obj);
64 // }}}
66 /**
67 * ActiveMongo
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
76 * @version 1.0
79 abstract class ActiveMongo implements Iterator
82 // properties {{{
83 /**
84 * Current collections
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 * Host name
104 * @type string
106 private static $_host;
108 * Current document
110 * @type array
112 private $_current = array();
114 * Result cursor
116 * @type MongoCursor
118 private $_cursor = null;
120 * Current document ID
122 * @type MongoID
124 private $_id;
125 // }}}
127 // string getCollectionName() {{{
129 * Get Collection Name, by default the class name,
130 * but you it can be override at the class itself to give
131 * a custom name.
133 * @return string Colleciton Name
135 protected function getCollectionName()
137 return strtolower(get_class($this));
139 // }}}
141 // void install() {{{
143 * Install.
145 * This static method iterate over the classes lists,
146 * and execute the setup() method on every ActiveMongo
147 * subclass. You should do this just once.
150 final public static function install()
152 $classes = array_reverse(get_declared_classes());
153 foreach ($classes as $class)
155 if ($class == __CLASS__) {
156 break;
158 if (is_subclass_of($class, __CLASS__)) {
159 $obj = new $class;
160 $obj->setup();
164 // }}}
166 // void connection($db, $host) {{{
168 * Connect
170 * This method setup parameters to connect to a MongoDB
171 * database. The connection is done when it is needed.
173 * @param string $db Database name
174 * @param string $host Host to connect
176 * @return void
178 final public static function connect($db, $host='localhost')
180 self::$_host = $host;
181 self::$_db = $db;
183 // }}}
185 // MongoConnection _getConnection() {{{
187 * Get Connection
189 * Get a valid database connection
191 * @return MongoConnection
193 final protected static function _getConnection()
195 if (is_null(self::$_conn)) {
196 self::$_conn = new Mongo(self::$_host);
198 return self::$_conn->selectDB(self::$_db);
200 // }}}
202 // MongoCollection _getCollection() {{{
204 * Get Collection
206 * Get a collection connection.
208 * @return MongoCollection
210 final protected function _getCollection()
212 $colName = $this->getCollectionName();
213 return self::_getConnection()->selectCollection($colName);
215 // }}}
217 // bool getCurrentSubDocument(array &$document, string $parent_key, array $values, array $past_values) {{{
219 * Generate Sub-document
221 * This method build the difference between the current sub-document,
222 * and the origin one. If there is no difference, it would do nothing,
223 * otherwise it would build a document containing the differences.
225 * @param array &$document Document target
226 * @param string $parent_key Parent key name
227 * @param array $values Current values
228 * @param array $past_values Original values
230 * @return false
232 function getCurrentSubDocument(&$document, $parent_key, Array $values, Array $past_values)
235 * The current property is a embedded-document,
236 * now we're looking for differences with the
237 * previous value (because we're on an update).
239 * It behaves exactly as getCurrentDocument,
240 * but this is simples (it doesn't support
241 * yet filters)
243 foreach ($values as $key => $value) {
244 $super_key = "{$parent_key}.{$key}";
245 if (is_array($value)) {
247 * Inner document detected
249 if (!isset($past_values[$key]) || !is_array($past_values[$key])) {
251 * We're lucky, it is a new sub-document,
252 * we simple add it
254 $document['$set'][$super_key] = $value;
255 } else {
257 * This is a document like this, we need
258 * to find out the differences to avoid
259 * network overhead.
261 if (!$this->getCurrentSubDocument($document, $super_key, $value, $past_values[$key])) {
262 return false;
265 continue;
267 if (!isset($past_values[$key]) || $past_values[$key] != $value) {
268 $document['$set'][$super_key] = $value;
272 foreach (array_diff(array_keys($past_values), array_keys($values)) as $key) {
273 $super_key = "{$parent_key}.{$key}";
274 $document['$unset'][$super_key] = 1;
277 return true;
279 // }}}
281 // array getCurrentDocument(bool $update) {{{
283 * Get Current Document
285 * Based on this object properties a new document (Array)
286 * is returned. If we're modifying an document, just the modified
287 * properties are included in this document, which uses $set,
288 * $unset, $pushAll and $pullAll.
291 * @param bool $update
293 * @return array
295 final protected function getCurrentDocument($update=false, $current=false)
297 $document = array();
298 $object = get_object_vars_ex($this);
300 if (!$current) {
301 $current = (array)$this->_current;
304 foreach ($object as $key => $value) {
305 if (!$value) {
306 continue;
308 if ($update) {
309 if (is_array($value) && isset($current[$key])) {
311 * If the Field to update is an array, it has a different
312 * behaviour other than $set and $unset. Fist, we need
313 * need to check if it is an array or document, because
314 * they can't be mixed.
317 if (!is_array($current[$key])) {
319 * We're lucky, the field wasn't
320 * an array previously.
322 $this->_call_filter($key, $value, $current[$key]);
323 $document['$set'][$key] = $value;
324 continue;
327 if (!$this->getCurrentSubDocument($document, $key, $value, $current[$key])) {
328 throw new Exception("{$key}: Array and documents are not compatible");
330 } else if(!isset($current[$key]) || $value !== $current[$key]) {
332 * It is 'linear' field that has changed, or
333 * has been modified.
335 $past_value = isset($current[$key]) ? $current[$key] : null;
336 $this->_call_filter($key, $value, $past_value);
337 $document['$set'][$key] = $value;
339 } else {
341 * It is a document insertation, so we
342 * create the document.
344 $this->_call_filter($key, $value, null);
345 $document[$key] = $value;
349 /* Updated behaves in a diff. way */
350 if ($update) {
351 foreach (array_diff(array_keys($this->_current), array_keys($object)) as $property) {
352 if ($property == '_id') {
353 continue;
355 $document['$unset'][$property] = 1;
359 if (count($document) == 0) {
360 return array();
362 return $document;
364 // }}}
366 // void _call_filter(string $key, mixed &$value, mixed $past_value) {{{
368 * *Internal Method*
370 * This method check if the current document property has
371 * a filter method, if so, call it.
373 * If the filter returns false, throw an Exception.
375 * @return void
377 private function _call_filter($key, &$value, $past_value)
379 $filter = array($this, "{$key}_filter");
380 if (is_callable($filter)) {
381 $filter = call_user_func_array($filter, array(&$value, $past_value));
382 if ($filter===false) {
383 throw new FilterException("{$key} filter failed");
385 $this->$key = $value;
388 // }}}
390 // void setCursor(MongoCursor $obj) {{{
392 * Set Cursor
394 * This method receive a MongoCursor and make
395 * it iterable.
397 * @param MongoCursor $obj
399 * @return void
401 final protected function setCursor(MongoCursor $obj)
403 $this->_cursor = $obj;
404 $this->setResult($obj->getNext());
406 // }}}
408 // void reset() {{{
410 * Reset our Object, delete the current cursor if any, and reset
411 * unsets the values.
413 * @return void
415 final function reset()
417 $this->_count = 0;
418 $this->_cursor = null;
419 $this->setResult(array());
421 // }}}
423 // void setResult(Array $obj) {{{
425 * Set Result
427 * This method takes an document and copy it
428 * as properties in this object.
430 * @param Array $obj
432 * @return void
434 final protected function setResult($obj)
436 /* Unsetting previous results, if any */
437 foreach (array_keys((array)$this->_current) as $key) {
438 unset($this->$key);
441 /* Add our current resultset as our object's property */
442 foreach ((array)$obj as $key => $value) {
443 if ($key[0] == '$') {
444 continue;
446 $this->$key = $value;
449 /* Save our record */
450 $this->_current = $obj;
452 // }}}
454 // this find([$_id]) {{{
456 * Simple find
458 * Really simple find, which uses this object properties
459 * for fast filtering
461 * @return object this
463 function find(MongoID $_id = null)
465 $vars = $this->getCurrentDocument();
466 if ($_id != null) {
467 $vars['_id'] = $_id;
469 $res = $this->_getCollection()->find($vars);
470 $this->setCursor($res);
471 return $this;
473 // }}}
475 // void save(bool $async) {{{
477 * Save
479 * This method save the current document in MongoDB. If
480 * we're modifying a document, a update is performed, otherwise
481 * the document is inserted.
483 * On updates, special operations such as $set, $pushAll, $pullAll
484 * and $unset in order to perform efficient updates
486 * @param bool $async
488 * @return void
490 final function save($async=true)
492 $update = isset($this->_id) && $this->_id InstanceOf MongoID;
493 $conn = $this->_getCollection();
494 $obj = $this->getCurrentDocument($update);
495 if (count($obj) == 0) {
496 return; /*nothing to do */
498 /* PRE-save hook */
499 $this->pre_save($update ? 'update' : 'create', $obj);
500 if ($update) {
501 $conn->update(array('_id' => $this->_id), $obj);
502 foreach ($obj as $key => $value) {
503 if ($key[0] == '$') {
504 continue;
506 $this->_current[$key] = $value;
508 $this->on_update();
509 } else {
510 $conn->insert($obj, $async);
511 $this->_id = $obj['_id'];
512 $this->_current = $obj;
513 $this->on_save();
516 // }}}
518 // bool delete() {{{
520 * Delete the current document
522 * @return bool
524 final function delete()
526 if ($this->valid()) {
527 return $this->_getCollection()->remove(array('_id' => $this->_id));
529 return false;
531 // }}}
533 // void drop() {{{
535 * Delete the current colleciton and all its documents
537 * @return void
539 final function drop()
541 $this->_getCollection()->drop();
542 $this->setResult(array());
543 $this->_cursor = null;
545 // }}}
547 // int count() {{{
549 * Return the number of documents in the actual request. If
550 * we're not in a request, it will return 0.
552 * @return int
554 final function count()
556 if ($this->valid()) {
557 return $this->_cursor->count();
559 return 0;
561 // }}}
563 // bool valid() {{{
565 * Valid
567 * Return if we're on an iteration and if it is still valid
569 * @return true
571 final function valid()
573 return $this->_cursor InstanceOf MongoCursor && $this->_cursor->valid();
575 // }}}
577 // bool next() {{{
579 * Move to the next document
581 * @return bool
583 final function next()
585 return $this->_cursor->next();
587 // }}}
589 // this current() {{{
591 * Return the current object, and load the current document
592 * as this object property
594 * @return object
596 final function current()
598 $this->setResult($this->_cursor->current());
599 return $this;
601 // }}}
603 // bool rewind() {{{
605 * Go to the first document
607 final function rewind()
609 return $this->_cursor->rewind();
611 // }}}
613 // getID() {{{
615 * Return the current document ID. If there is
616 * no document it would return false.
618 * @return object|false
620 final public function getID()
622 if ($this->_id instanceof MongoID) {
623 return $this->_id;
625 return false;
627 // }}}
629 // string key() {{{
631 * Return the current key
633 * @return string
635 final function key()
637 return $this->getID();
639 // }}}
641 // void pre_save($action, & $document) {{{
643 * PRE-save Hook,
645 * This method is fired just before an insert or updated. The document
646 * is passed by reference, so it can be modified. Also if for instance
647 * one property is missing an Exception could be thrown to avoid
648 * the insert.
651 * @param string $action Update or Create
652 * @param array &$document Document that will be sent to MongoDB.
654 * @return void
656 protected function pre_save($action, Array &$document)
659 // }}}
661 // void on_save() {{{
663 * On Save hook
665 * This method is fired right after an insert is performed.
667 * @return void
669 protected function on_save()
672 // }}}
674 // void on_update() {{{
676 * On Update hook
678 * This method is fired right after an update is performed.
680 * @return void
682 protected function on_update()
685 // }}}
687 // void on_iterate() {{{
689 * On Iterate Hook
691 * This method is fired right after a new document is loaded
692 * from the recorset, it could be useful to load references to other
693 * documents.
695 * @return void
697 protected function on_iterate()
700 // }}}
702 // setup() {{{
704 * This method should contain all the indexes, and shard keys
705 * needed by the current collection. This try to make
706 * installation on development environments easier.
708 function setup()
711 // }}}
716 * Local variables:
717 * tab-width: 4
718 * c-basic-offset: 4
719 * End:
720 * vim600: sw=4 ts=4 fdm=marker
721 * vim<600: sw=4 ts=4