Prevented specials document's properties being part of the current object.
[activemongo.git] / ActiveMongo.php
blobacf277a6228047bd757451f8d0c3deb07dc232be
1 <?php
2 /*
3 +----------------------------------------------------------------------+
4 | Copyright (c) 2009 The PHP Group |
5 +----------------------------------------------------------------------+
6 | This source file is subject to version 3.0 of the PHP license, |
7 | that is bundled with this package in the file LICENSE, and is |
8 | available through the world-wide-web at the following url: |
9 | http://www.php.net/license/3_0.txt. |
10 | If you did not receive a copy of the PHP license and are unable to |
11 | obtain it through the world-wide-web, please send a note to |
12 | license@php.net so we can mail you a copy immediately. |
13 +----------------------------------------------------------------------+
14 | Authors: Cesar Rodas <crodas@php.net> |
15 +----------------------------------------------------------------------+
19 // Class FilterException {{{
20 /**
21 * FilterException
23 * This is Exception is thrown if any validation
24 * fails when save() is called.
27 final class FilterException extends Exception
30 // }}}
32 // array get_object_vars_ex(stdobj $obj) {{{
33 /**
34 * Simple hack to avoid get private and protected variables
36 * @param obj
38 * @return array
40 function get_object_vars_ex($obj)
42 return get_object_vars($obj);
44 // }}}
46 /**
47 * ActiveMongo
49 * Simple ActiveRecord pattern built on top of MongoDB. This class
50 * aims to provide easy iteration, data validation before update,
51 * and efficient update.
53 * @author César D. Rodas <crodas@php.net>
54 * @license PHP License
55 * @package ActiveMongo
56 * @version 1.0
59 abstract class ActiveMongo implements Iterator
62 // properties {{{
63 /**
64 * Current collections
66 * @type array
68 private static $_collections;
69 /**
70 * Current connection to MongoDB
72 * @type MongoConnection
74 private static $_conn;
75 /**
76 * Database name
78 * @type string
80 private static $_db;
81 /**
82 * Host name
84 * @type string
86 private static $_host;
87 /**
88 * Current document
90 * @type array
92 private $_current = array();
93 /**
94 * Result cursor
96 * @type MongoCursor
98 private $_cursor = null;
99 /**
100 * Current document ID
102 * @type MongoID
104 private $_id;
105 // }}}
107 // string getCollectionName() {{{
109 * Get Collection Name, by default the class name,
110 * but you it can be override at the class itself to give
111 * a custom name.
113 * @return string Colleciton Name
115 protected function getCollectionName()
117 return strtolower(get_class($this));
119 // }}}
121 // void install() {{{
123 * Install.
125 * This static method iterate over the classes lists,
126 * and execute the setup() method on every ActiveMongo
127 * subclass. You should do this just once.
130 final public static function install()
132 $classes = array_reverse(get_declared_classes());
133 foreach ($classes as $class)
135 if ($class == __CLASS__) {
136 break;
138 if (is_subclass_of($class, __CLASS__)) {
139 $obj = new $class;
140 $obj->setup();
144 // }}}
146 // void connection($db, $host) {{{
148 * Connect
150 * This method setup parameters to connect to a MongoDB
151 * database. The connection is done when it is needed.
153 * @param string $db Database name
154 * @param string $host Host to connect
156 * @return void
158 final public static function connect($db, $host='localhost')
160 self::$_host = $host;
161 self::$_db = $db;
163 // }}}
165 // MongoConnection _getConnection() {{{
167 * Get Connection
169 * Get a valid database connection
171 * @return MongoConnection
173 final protected static function _getConnection()
175 if (is_null(self::$_conn)) {
176 self::$_conn = new Mongo(self::$_host);
178 return self::$_conn->selectDB(self::$_db);
180 // }}}
182 // MongoCollection _getCollection() {{{
184 * Get Collection
186 * Get a collection connection.
188 * @return MongoCollection
190 final protected function _getCollection()
192 $colName = $this->getCollectionName();
193 return self::_getConnection()->selectCollection($colName);
195 // }}}
197 // bool getCurrentSubDocument(array &$document, string $parent_key, array $values, array $past_values) {{{
199 * Generate Sub-document
201 * This method build the difference between the current sub-document,
202 * and the origin one. If there is no difference, it would do nothing,
203 * otherwise it would build a document containing the differences.
205 * @param array &$document Document target
206 * @param string $parent_key Parent key name
207 * @param array $values Current values
208 * @param array $past_values Original values
210 * @return false
212 function getCurrentSubDocument(&$document, $parent_key, Array $values, Array $past_values)
215 * The current property is a embedded-document,
216 * now we're looking for differences with the
217 * previous value (because we're on an update).
219 * It behaves exactly as getCurrentDocument,
220 * but this is simples (it doesn't support
221 * yet filters)
223 foreach ($values as $key => $value) {
224 $super_key = "{$parent_key}.{$key}";
225 if (is_array($value)) {
227 * Inner document detected
229 if (!isset($past_values[$key]) || !is_array($past_values[$key])) {
231 * We're lucky, it is a new sub-document,
232 * we simple add it
234 $document['$set'][$super_key] = $value;
235 } else {
237 * This is a document like this, we need
238 * to find out the differences to avoid
239 * network overhead.
241 if (!$this->getCurrentSubDocument($document, $super_key, $value, $past_values[$key])) {
242 return false;
245 continue;
247 if (!isset($past_values[$key]) || $past_values[$key] != $value) {
248 $document['$set'][$super_key] = $value;
252 foreach (array_diff(array_keys($past_values), array_keys($values)) as $key) {
253 $super_key = "{$parent_key}.{$key}";
254 $document['$unset'][$super_key] = 1;
257 return true;
259 // }}}
261 // array getCurrentDocument(bool $update) {{{
263 * Get Current Document
265 * Based on this object properties a new document (Array)
266 * is returned. If we're modifying an document, just the modified
267 * properties are included in this document, which uses $set,
268 * $unset, $pushAll and $pullAll.
271 * @param bool $update
273 * @return array
275 final protected function getCurrentDocument($update=false, $current=false)
277 $document = array();
278 $object = get_object_vars_ex($this);
280 if (!$current) {
281 $current = (array)$this->_current;
284 foreach ($object as $key => $value) {
285 if (!$value) {
286 continue;
288 if ($update) {
289 if (is_array($value) && isset($current[$key])) {
291 * If the Field to update is an array, it has a different
292 * behaviour other than $set and $unset. Fist, we need
293 * need to check if it is an array or document, because
294 * they can't be mixed.
297 if (!is_array($current[$key])) {
299 * We're lucky, the field wasn't
300 * an array previously.
302 $this->_call_filter($key, $value, $current[$key]);
303 $document['$set'][$key] = $value;
304 continue;
307 if (!$this->getCurrentSubDocument($document, $key, $value, $current[$key])) {
308 throw new Exception("{$key}: Array and documents are not compatible");
310 } else if(!isset($current[$key]) || $value !== $current[$key]) {
312 * It is 'linear' field that has changed, or
313 * has been modified.
315 $past_value = isset($current[$key]) ? $current[$key] : null;
316 $this->_call_filter($key, $value, $past_value);
317 $document['$set'][$key] = $value;
319 } else {
321 * It is a document insertation, so we
322 * create the document.
324 $this->_call_filter($key, $value, null);
325 $document[$key] = $value;
329 /* Updated behaves in a diff. way */
330 if ($update) {
331 foreach (array_diff(array_keys($this->_current), array_keys($object)) as $property) {
332 if ($property == '_id') {
333 continue;
335 $document['$unset'][$property] = 1;
339 if (count($document) == 0) {
340 return array();
342 return $document;
344 // }}}
346 // void _call_filter(string $key, mixed &$value, mixed $past_value) {{{
348 * *Internal Method*
350 * This method check if the current document property has
351 * a filter method, if so, call it.
353 * If the filter returns false, throw an Exception.
355 * @return void
357 private function _call_filter($key, &$value, $past_value)
359 $filter = array($this, "{$key}_filter");
360 if (is_callable($filter)) {
361 $filter = call_user_func_array($filter, array(&$value, $past_value));
362 if ($filter===false) {
363 throw new FilterException("{$key} filter failed");
365 $this->$key = $value;
368 // }}}
370 // void setCursor(MongoCursor $obj) {{{
372 * Set Cursor
374 * This method receive a MongoCursor and make
375 * it iterable.
377 * @param MongoCursor $obj
379 * @return void
381 final protected function setCursor(MongoCursor $obj)
383 $this->_cursor = $obj;
384 $this->setResult($obj->getNext());
386 // }}}
388 // void reset() {{{
390 * Reset our Object, delete the current cursor if any, and reset
391 * unsets the values.
393 * @return void
395 final function reset()
397 $this->_count = 0;
398 $this->_cursor = null;
399 $this->setResult(array());
401 // }}}
403 // void setResult(Array $obj) {{{
405 * Set Result
407 * This method takes an document and copy it
408 * as properties in this object.
410 * @param Array $obj
412 * @return void
414 final protected function setResult($obj)
416 /* Unsetting previous results, if any */
417 foreach (array_keys((array)$this->_current) as $key) {
418 unset($this->$key);
421 /* Add our current resultset as our object's property */
422 foreach ((array)$obj as $key => $value) {
423 if ($key[0] == '$') {
424 continue;
426 $this->$key = $value;
429 /* Save our record */
430 $this->_current = $obj;
432 // }}}
434 // this find([$_id]) {{{
436 * Simple find
438 * Really simple find, which uses this object properties
439 * for fast filtering
441 * @return object this
443 function find(MongoID $_id = null)
445 $vars = $this->getCurrentDocument();
446 if ($_id != null) {
447 $vars['_id'] = $_id;
449 $res = $this->_getCollection()->find($vars);
450 $this->setCursor($res);
451 return $this;
453 // }}}
455 // void save(bool $async) {{{
457 * Save
459 * This method save the current document in MongoDB. If
460 * we're modifying a document, a update is performed, otherwise
461 * the document is inserted.
463 * On updates, special operations such as $set, $pushAll, $pullAll
464 * and $unset in order to perform efficient updates
466 * @param bool $async
468 * @return void
470 final function save($async=true)
472 $update = isset($this->_id) && $this->_id InstanceOf MongoID;
473 $conn = $this->_getCollection();
474 $obj = $this->getCurrentDocument($update);
475 if (count($obj) == 0) {
476 return; /*nothing to do */
478 /* PRE-save hook */
479 $this->pre_save($update ? 'update' : 'create', $obj);
480 if ($update) {
481 $conn->update(array('_id' => $this->_id), $obj);
482 foreach ($obj as $key => $value) {
483 if ($key[0] == '$') {
484 continue;
486 $this->_current[$key] = $value;
488 $this->on_update();
489 } else {
490 $conn->insert($obj, $async);
491 $this->_id = $obj['_id'];
492 $this->_current = $obj;
493 $this->on_save();
496 // }}}
498 // bool delete() {{{
500 * Delete the current document
502 * @return bool
504 final function delete()
506 if ($this->valid()) {
507 return $this->_getCollection()->remove(array('_id' => $this->_id));
509 return false;
511 // }}}
513 // void drop() {{{
515 * Delete the current colleciton and all its documents
517 * @return void
519 final function drop()
521 $this->_getCollection()->drop();
522 $this->setResult(array());
523 $this->_cursor = null;
525 // }}}
527 // int count() {{{
529 * Return the number of documents in the actual request. If
530 * we're not in a request, it will return 0.
532 * @return int
534 final function count()
536 if ($this->valid()) {
537 return $this->_cursor->count();
539 return 0;
541 // }}}
543 // bool valid() {{{
545 * Valid
547 * Return if we're on an iteration and if it is still valid
549 * @return true
551 final function valid()
553 return $this->_cursor InstanceOf MongoCursor && $this->_cursor->valid();
555 // }}}
557 // bool next() {{{
559 * Move to the next document
561 * @return bool
563 final function next()
565 return $this->_cursor->next();
567 // }}}
569 // this current() {{{
571 * Return the current object, and load the current document
572 * as this object property
574 * @return object
576 final function current()
578 $this->setResult($this->_cursor->current());
579 return $this;
581 // }}}
583 // bool rewind() {{{
585 * Go to the first document
587 final function rewind()
589 return $this->_cursor->rewind();
591 // }}}
593 // getID() {{{
595 * Return the current document ID. If there is
596 * no document it would return false.
598 * @return object|false
600 final public function getID()
602 if ($this->_id instanceof MongoID) {
603 return $this->_id;
605 return false;
607 // }}}
609 // string key() {{{
611 * Return the current key
613 * @return string
615 final function key()
617 return $this->getID();
619 // }}}
621 // void pre_save($action, & $document) {{{
623 * PRE-save Hook,
625 * This method is fired just before an insert or updated. The document
626 * is passed by reference, so it can be modified. Also if for instance
627 * one property is missing an Exception could be thrown to avoid
628 * the insert.
631 * @param string $action Update or Create
632 * @param array &$document Document that will be sent to MongoDB.
634 * @return void
636 protected function pre_save($action, Array &$document)
639 // }}}
641 // void on_save() {{{
643 * On Save hook
645 * This method is fired right after an insert is performed.
647 * @return void
649 protected function on_save()
652 // }}}
654 // void on_update() {{{
656 * On Update hook
658 * This method is fired right after an update is performed.
660 * @return void
662 protected function on_update()
665 // }}}
667 // void on_iterate() {{{
669 * On Iterate Hook
671 * This method is fired right after a new document is loaded
672 * from the recorset, it could be useful to load references to other
673 * documents.
675 * @return void
677 protected function on_iterate()
680 // }}}
682 // setup() {{{
684 * This method should contain all the indexes, and shard keys
685 * needed by the current collection. This try to make
686 * installation on development environments easier.
688 function setup()
691 // }}}
696 * Local variables:
697 * tab-width: 4
698 * c-basic-offset: 4
699 * End:
700 * vim600: sw=4 ts=4 fdm=marker
701 * vim<600: sw=4 ts=4