Rev 14098 | Blame | Compare with Previous | Last modification | View Log | RSS feed
<?php/*** A CakePHP datasource for the mongoDB (http://www.mongodb.org/) document-oriented database.** This datasource uses Pecl Mongo (http://php.net/mongo)* and is thus dependent on PHP 5.0 and greater.** Original implementation by ichikaway(Yasushi Ichikawa) http://github.com/ichikaway/** Reference:* Nate Abele's lithium mongoDB datasource (http://li3.rad-dev.org/)* Joél Perras' divan(http://github.com/jperras/divan/)** Copyright 2010, Yasushi Ichikawa http://github.com/ichikaway/** Contributors: Predominant, Jrbasso, tkyk, AD7six** Licensed under The MIT License* Redistributions of files must retain the above copyright notice.** @copyright Copyright 2010, Yasushi Ichikawa http://github.com/ichikaway/* @package mongodb* @subpackage mongodb.models.datasources* @license http://www.opensource.org/licenses/mit-license.php The MIT License*/App::uses('DboSource', 'Model/Datasource');App::uses('SchemalessBehavior', 'Mongodb.Model/Behavior');/*** MongoDB Source** @package mongodb* @subpackage mongodb.models.datasources*/class MongodbSource extends DboSource {/*** Are we connected to the DataSource?** true - yes* null - haven't tried yet* false - nope, and we can't connect** @var boolean* @access public*/public $connected = null;/*** Database Instance** @var resource* @access protected*/protected $_db = null;/*** Mongo Driver Version** @var string* @access protected*/protected $_driverVersion = Mongo::VERSION;/*** startTime property** If debugging is enabled, stores the (micro)time the current query started** @var mixed null* @access protected*/protected $_startTime = null;/*** Direct connection with database, isn't the* same of DboSource::_connection** @var mixed null | Mongo* @access private*/public $connection = null;/*** Base Config** set_string_id:* true: In read() method, convert MongoId object to string and set it to array 'id'.* false: not convert and set.** @var array* @access public**/public $_baseConfig = array('set_string_id' => true,'persistent' => true,'host' => 'localhost','database' => '','port' => '27017','login' => '','password' => '','replicaset' => '',);/*** column definition** @var array*/public $columns = array('boolean' => array('name' => 'boolean'),'string' => array('name' => 'varchar'),'text' => array('name' => 'text'),'integer' => array('name' => 'integer', 'format' => null, 'formatter' => 'intval'),'float' => array('name' => 'float', 'format' => null, 'formatter' => 'floatval'),'datetime' => array('name' => 'datetime', 'format' => null, 'formatter' => 'MongodbDateFormatter'),'timestamp' => array('name' => 'timestamp', 'format' => null, 'formatter' => 'MongodbDateFormatter'),'time' => array('name' => 'time', 'format' => null, 'formatter' => 'MongodbDateFormatter'),'date' => array('name' => 'date', 'format' => null, 'formatter' => 'MongodbDateFormatter'),);/*** Default schema for the mongo models** @var array* @access protected*/protected $_defaultSchema = array('_id' => array('type' => 'string', 'length' => 24, 'key' => 'primary'),'created' => array('type' => 'datetime', 'default' => null),'modified' => array('type' => 'datetime', 'default' => null));/*** construct method** By default don't try to connect until you need to** @param array $config Configuration array* @param bool $autoConnect false* @return void* @access public*/function __construct($config = array(), $autoConnect = false) {return parent::__construct($config, $autoConnect);}/*** Destruct** @access public*/public function __destruct() {if ($this->connected) {$this->disconnect();}}/*** commit method** MongoDB doesn't support transactions** @return void* @access public*/public function commit() {return false;}/*** Connect to the database** If using 1.0.2 or above use the mongodb:// format to connect* The connect syntax changed in version 1.0.2 - so check for that too** If authentication information in present then authenticate the connection** @return boolean Connected* @access public*/public function connect() {$this->connected = false;try{$host = $this->createConnectionName($this->config, $this->_driverVersion);$class = 'MongoClient';if(!class_exists($class)){$class = 'Mongo';}if (isset($this->config['replicaset']) && count($this->config['replicaset']) === 2) {$this->connection = new $class($this->config['replicaset']['host'], $this->config['replicaset']['options']);} else if ($this->_driverVersion >= '1.3.0') {$this->connection = new $class($host);} else if ($this->_driverVersion >= '1.2.0') {$this->connection = new $class($host, array("persist" => $this->config['persistent']));} else {$this->connection = new $class($host, true, $this->config['persistent']);}if (isset($this->config['slaveok'])) {if (method_exists($this->connection, 'setSlaveOkay')) {$this->connection->setSlaveOkay($this->config['slaveok']);} else {$this->connection->setReadPreference($this->config['slaveok']? $class::RP_SECONDARY_PREFERRED : $class::RP_PRIMARY);}}if ($this->_db = $this->connection->selectDB($this->config['database'])) {if (!empty($this->config['login']) && $this->_driverVersion < '1.2.0') {$return = $this->_db->authenticate($this->config['login'], $this->config['password']);if (!$return || !$return['ok']) {trigger_error('MongodbSource::connect ' . $return['errmsg']);return false;}}$this->connected = true;}} catch(MongoException $e) {$this->error = $e->getMessage();trigger_error($this->error);}return $this->connected;}/*** create connection name.** @param array $config* @param string $version version of MongoDriver*/public function createConnectionName($config, $version) {$host = null;if ($version >= '1.0.2') {$host = "mongodb://";} else {$host = '';}$hostname = $config['host'] . ':' . $config['port'];if(!empty($config['login'])){$host .= $config['login'] .':'. $config['password'] . '@' . $hostname . '/'. $config['database'];} else {$host .= $hostname;}return $host;}/*** Inserts multiple values into a table** @param string $table* @param string $fields* @param array $values* @access public*/public function insertMulti($table, $fields, $values) {$table = $this->fullTableName($table);if (!is_array($fields) || !is_array($values)) {return false;}$inUse = array_search('id', $fields);$default = array_search('_id', $fields);if ($inUse !== false && $default === false) {$fields[$inUse] = '_id';}$values = $this->normalizeValues($table, $fields, $values);$data = array();foreach ($values as $row) {if (is_string($row)) {$row = explode(', ', substr($row, 1, -1));}$data[] = array_combine($fields, $row);}$this->_prepareLogQuery($table); // just sets a timertry{$return = $this->_db->selectCollection($table)->batchInsert($data, array('w' => 1));} catch (MongoException $e) {$this->error = $e->getMessage();trigger_error($this->error);}if ($this->fullDebug) {$this->logQuery("db.{$table}.insertMulti( :data , array('w' => 1))", compact('data'));}}public function normalizeValues($table, $fields, $values) {$Model = ClassRegistry::init(Inflector::classify($table));foreach ($values as $key => $value) {foreach ($value as $k => $v) {switch($Model->mongoSchema[$fields[$k]]['type']) {case 'datetime':case 'timestamp':case 'date':case 'time':if (is_string($values[$key][$k])) {$values[$key][$k] = new MongoDate(strtotime($v));}break;default:break;}}}return $values;}/*** check connection to the database** @return boolean Connected* @access public*/public function isConnected() {if ($this->connected === false) {return false;}return $this->connect();}/*** get MongoDB Object** @return mixed MongoDB Object* @access public*/public function getMongoDb() {if ($this->connected === false) {return false;}return $this->_db;}/*** get MongoDB Collection Object** @return mixed MongoDB Collection Object* @access public*/public function getMongoCollection(&$Model) {if ($this->connected === false) {return false;}$table = $this->fullTableName($Model);$collection = $this->_db->selectCollection($table);return $collection;}/*** isInterfaceSupported method** listSources is infact supported, however: cake expects it to return a complete list of all* possible sources in the selected db - the possible list of collections is infinte, so it's* faster and simpler to tell cake that the interface is /not/ supported so it assumes that* <insert name of your table here> exist** @param mixed $interface* @return void* @access public*/public function isInterfaceSupported($interface) {if ($interface === 'listSources') {return false;}return parent::isInterfaceSupported($interface);}/*** Close database connection** @return boolean Connected* @access public*/public function close() {return $this->disconnect();}/*** Disconnect from the database** @return boolean Connected* @access public*/public function disconnect() {if ($this->connected) {$this->connected = !$this->connection->close();unset($this->_db, $this->connection);return !$this->connected;}return true;}/*** Get list of available Collections** @param array $data* @return array Collections* @access public*/public function listSources($data = null) {if (!$this->isConnected()) {return false;}return true;}/*** Describe** Automatically bind the schemaless behavior if there is no explicit mongo schema.* When called, if there is model data it will be used to derive a schema. a row is plucked* out of the db and the data obtained used to derive the schema.** @param Model $Model* @return array if model instance has mongoSchema, return it.* @access public*/public function describe($Model) {if(empty($Model->primaryKey)) {$Model->primaryKey = '_id';}$schema = array();$table = $this->fullTableName($Model);if (!empty($Model->mongoSchema) && is_array($Model->mongoSchema)) {$schema = $Model->mongoSchema;return $schema + array($Model->primaryKey => $this->_defaultSchema['_id']);} elseif ($this->isConnected() && is_a($Model, 'Model') && !empty($Model->Behaviors)) {$Model->Behaviors->attach('Mongodb.Schemaless');if (!$Model->data) {if ($this->_db->selectCollection($table)->count()) {return $this->deriveSchemaFromData($Model, $this->_db->selectCollection($table)->findOne());}}}return $this->deriveSchemaFromData($Model);}/*** begin method** Mongo doesn't support transactions** @return void* @access public*/public function begin() {return false;}/*** Calculate** @param Model $Model* @return array* @access public*/public function calculate(Model $Model, $func, $params = array()) {return array('count' => true);}/*** Quotes identifiers.** MongoDb does not need identifiers quoted, so this method simply returns the identifier.** @param string $name The identifier to quote.* @return string The quoted identifier.*/public function name($name) {return $name;}/*** Create Data** @param Model $Model Model Instance* @param array $fields Field data* @param array $values Save data* @return boolean Insert result* @access public*/public function create(Model $Model, $fields = null, $values = null) {if (!$this->isConnected()) {return false;}if ($fields !== null && $values !== null) {$data = array_combine($fields, $values);} else {$data = $Model->data;}if($Model->primaryKey !== '_id' && isset($data[$Model->primaryKey]) && !empty($data[$Model->primaryKey])) {$data['_id'] = $data[$Model->primaryKey];unset($data[$Model->primaryKey]);}if (!empty($data['_id'])) {$this->_convertId($data['_id']);}$this->_prepareLogQuery($Model); // just sets a timer$table = $this->fullTableName($Model);try{if ($this->_driverVersion >= '1.3.0') {$return = $this->_db->selectCollection($table)->insert($data, array('safe' => true));} else {$return = $this->_db->selectCollection($table)->insert($data, true);}} catch (MongoException $e) {$this->error = $e->getMessage();trigger_error($this->error);}if ($this->fullDebug) {$this->logQuery("db.{$table}.insert( :data , true)", compact('data'));}if (!empty($return) && $return['ok']) {$id = $data['_id'];if($this->config['set_string_id'] && is_object($data['_id'])) {$id = $data['_id']->__toString();}$Model->setInsertID($id);$Model->id = $id;return true;}return false;}/*** createSchema method** Mongo no care for creating schema. Mongo work with no schema.** @param mixed $schema* @param mixed $tableName null* @return void* @access public*/public function createSchema($schema, $tableName = null) {return true;}/*** dropSchema method** Return a command to drop each table** @param mixed $schema* @param mixed $tableName null* @return void* @access public*/public function dropSchema(CakeSchema $schema, $tableName = null) {if (!$this->isConnected()) {return false;}if (!is_a($schema, 'CakeSchema')) {trigger_error(__('Invalid schema object', true), E_USER_WARNING);return null;}if ($tableName) {return "db.{$tableName}.drop();";}$toDrop = array();foreach ($schema->tables as $curTable => $columns) {if ($tableName === $curTable) {$toDrop[] = $curTable;}}if (count($toDrop) === 1) {return "db.{$toDrop[0]}.drop();";}$return = "toDrop = :tables;\nfor( i = 0; i < toDrop.length; i++ ) {\n\tdb[toDrop[i]].drop();\n}";$tables = '["' . implode($toDrop, '", "') . '"]';return String::insert($return, compact('tables'));}/*** distinct method** @param mixed $Model* @param array $keys array()* @param array $params array()* @return void* @access public*/public function distinct(&$Model, $keys = array(), $params = array()) {if (!$this->isConnected()) {return false;}$this->_prepareLogQuery($Model); // just sets a timerif (array_key_exists('conditions', $params)) {$params = $params['conditions'];}$table = $this->fullTableName($Model);try{$return = $this->_db->selectCollection($table)->distinct($keys, $params);} catch (MongoException $e) {$this->error = $e->getMessage();trigger_error($this->error);}if ($this->fullDebug) {$this->logQuery("db.{$table}.distinct( :keys, :params )", compact('keys', 'params'));}return $return;}/*** group method** @param array $params array()* Set params same as MongoCollection::group()* key,initial, reduce, options(conditions, finalize)** Ex. $params = array(* 'key' => array('field' => true),* 'initial' => array('csum' => 0),* 'reduce' => 'function(obj, prev){prev.csum += 1;}',* 'options' => array(* 'condition' => array('age' => array('$gt' => 20)),* 'finalize' => array(),* ),* );* @param mixed $Model* @return void* @access public*///public function group($params, Model $Model = null) {public function group($params, $Model = null) {if (!$this->isConnected() || count($params) === 0 || $Model === null) {return false;}$this->_prepareLogQuery($Model); // just sets a timer$key = (empty($params['key'])) ? array() : $params['key'];$initial = (empty($params['initial'])) ? array() : $params['initial'];$reduce = (empty($params['reduce'])) ? array() : $params['reduce'];$options = (empty($params['options'])) ? array() : $params['options'];$table = $this->fullTableName($Model);try{$return = $this->_db->selectCollection($table)->group($key, $initial, $reduce, $options);} catch (MongoException $e) {$this->error = $e->getMessage();trigger_error($this->error);}if ($this->fullDebug) {$this->logQuery("db.{$table}.group( :key, :initial, :reduce, :options )", $params);}return $return;}/*** ensureIndex method** @param mixed $Model* @param array $keys array()* @param array $params array()* @return void* @access public*/public function ensureIndex(&$Model, $keys = array(), $params = array()) {if (!$this->isConnected()) {return false;}$this->_prepareLogQuery($Model); // just sets a timer$table = $this->fullTableName($Model);try{$return = $this->_db->selectCollection($table)->ensureIndex($keys, $params);} catch (MongoException $e) {$this->error = $e->getMessage();trigger_error($this->error);}if ($this->fullDebug) {$this->logQuery("db.{$table}.ensureIndex( :keys, :params )", compact('keys', 'params'));}return $return;}/*** Update Data** This method uses $set operator automatically with MongoCollection::update().* If you don't want to use $set operator, you can chose any one as follw.* 1. Set TRUE in Model::mongoNoSetOperator property.* 2. Set a mongodb operator in a key of save data as follow.* Model->save(array('_id' => $id, '$inc' => array('count' => 1)));* Don't use Model::mongoSchema property,* CakePHP delete '$inc' data in Model::Save().* 3. Set a Mongo operator in Model::mongoNoSetOperator property.* Model->mongoNoSetOperator = '$inc';* Model->save(array('_id' => $id, array('count' => 1)));** @param Model $Model Model Instance* @param array $fields Field data* @param array $values Save data* @return boolean Update result* @access public*/public function update(Model $Model, $fields = null, $values = null, $conditions = null) {if (!$this->isConnected()) {return false;}if ($fields !== null && $values !== null) {$data = array_combine($fields, $values);} elseif($fields !== null && $conditions !== null) {return $this->updateAll($Model, $fields, $conditions);} else{$data = $Model->data;}if($Model->primaryKey !== '_id' && isset($data[$Model->primaryKey]) && !empty($data[$Model->primaryKey])) {$data['_id'] = $data[$Model->primaryKey];unset($data[$Model->primaryKey]);}if (empty($data['_id'])) {$data['_id'] = $Model->id;}$this->_convertId($data['_id']);$table = $this->fullTableName($Model);try{$mongoCollectionObj = $this->_db->selectCollection($table);} catch (MongoException $e) {$this->error = $e->getMessage();trigger_error($this->error);return false;}$this->_prepareLogQuery($Model); // just sets a timerif (!empty($data['_id'])) {$this->_convertId($data['_id']);$cond = array('_id' => $data['_id']);unset($data['_id']);$data = $this->setMongoUpdateOperator($Model, $data);try{if ($this->_driverVersion >= '1.3.0') {$return = $mongoCollectionObj->update($cond, $data, array("multiple" => false, 'safe' => true));} else {$return = $mongoCollectionObj->update($cond, $data, array("multiple" => false));}} catch (MongoException $e) {$this->error = $e->getMessage();trigger_error($this->error);}if ($this->fullDebug) {$this->logQuery("db.{$table}.update( :conditions, :data, :params )",array('conditions' => $cond, 'data' => $data, 'params' => array("multiple" => false)));}} else {try{if ($this->_driverVersion >= '1.3.0') {$return = $mongoCollectionObj->save($data, array('safe' => true));} else {$return = $mongoCollectionObj->save($data);}} catch (MongoException $e) {$this->error = $e->getMessage();trigger_error($this->error);}if ($this->fullDebug) {$this->logQuery("db.{$table}.save( :data )", compact('data'));}}return $return;}/*** setMongoUpdateOperator** Set Mongo update operator following saving data.* This method is for update() and updateAll.** @param Model $Model Model Instance* @param array $values Save data* @return array $data* @access public*/public function setMongoUpdateOperator(&$Model, $data) {if(isset($data['updated'])) {$updateField = 'updated';} else {$updateField = 'modified';}//setting Mongo operatorif(empty($Model->mongoNoSetOperator)) {if(!preg_grep('/^\$/', array_keys($data))) {$data = array('$set' => $data);} else {if(!empty($data[$updateField])) {$modified = $data[$updateField];unset($data[$updateField]);$data['$set'] = array($updateField => $modified);}}} elseif(substr($Model->mongoNoSetOperator,0,1) === '$') {if(!empty($data[$updateField])) {$modified = $data[$updateField];unset($data[$updateField]);$data = array($Model->mongoNoSetOperator => $data, '$set' => array($updateField => $modified));} else {$data = array($Model->mongoNoSetOperator => $data);}}return $data;}/*** Update multiple Record** @param Model $Model Model Instance* @param array $fields Field data* @param array $conditions* @return boolean Update result* @access public*/public function updateAll(&$Model, $fields = null, $conditions = null) {if (!$this->isConnected()) {return false;}$this->_stripAlias($conditions, $Model->alias);$this->_stripAlias($fields, $Model->alias, false, 'value');$fields = $this->setMongoUpdateOperator($Model, $fields);$this->_prepareLogQuery($Model); // just sets a timer$table = $this->fullTableName($Model);try{if ($this->_driverVersion >= '1.3.0') {// not use 'upsert'$return = $this->_db->selectCollection($table)->update($conditions, $fields, array("multiple" => true, 'safe' => true));if (isset($return['updatedExisting'])) {$return = $return['updatedExisting'];}} else {$return = $this->_db->selectCollection($table)->update($conditions, $fields, array("multiple" => true));}} catch (MongoException $e) {$this->error = $e->getMessage();trigger_error($this->error);}if ($this->fullDebug) {$this->logQuery("db.{$table}.update( :conditions, :fields, :params )",array('conditions' => $conditions, 'fields' => $fields, 'params' => array("multiple" => true)));}return $return;}/*** deriveSchemaFromData method** @param mixed $Model* @param array $data array()* @return void* @access public*/public function deriveSchemaFromData($Model, $data = array()) {if (!$data) {$data = $Model->data;if ($data && array_key_exists($Model->alias, $data)) {$data = $data[$Model->alias];}}$return = $this->_defaultSchema;if ($data) {$fields = array_keys($data);foreach($fields as $field) {if (in_array($field, array('created', 'modified', 'updated'))) {$return[$field] = array('type' => 'datetime', 'null' => true);} else {$return[$field] = array('type' => 'string', 'length' => 2000);}}}return $return;}/*** Delete Data** For deleteAll(true, false) calls - conditions will arrive here as true - account for that and* convert to an empty array* For deleteAll(array('some conditions')) calls - conditions will arrive here as:* array(* Alias._id => array(1, 2, 3, ...)* )** This format won't be understood by mongodb, it'll find 0 rows. convert to:** array(* Alias._id => array('$in' => array(1, 2, 3, ...))* )** @TODO bench remove() v drop. if it's faster to drop - just drop the collection taking into* account existing indexes (recreate just the indexes)* @param Model $Model Model Instance* @param array $conditions* @return boolean Update result* @access public*/public function delete(Model $Model, $conditions = null) {if (!$this->isConnected()) {return false;}$id = null;$this->_stripAlias($conditions, $Model->alias);if ($conditions === true) {$conditions = array();} elseif (empty($conditions)) {$id = $Model->id;} elseif (!empty($conditions) && !is_array($conditions)) {$id = $conditions;$conditions = array();} elseif (!empty($conditions['id'])) { //for cakephp2.0$id = $conditions['id'];unset($conditions['id']);}$table = $this->fullTableName($Model);$mongoCollectionObj = $this->_db->selectCollection($table);$this->_stripAlias($conditions, $Model->alias);if (!empty($id)) {$conditions['_id'] = $id;}if (!empty($conditions['_id'])) {$this->_convertId($conditions['_id'], true);}$return = false;$r = false;try{$this->_prepareLogQuery($Model); // just sets a timer$return = $mongoCollectionObj->remove($conditions);if ($this->fullDebug) {$this->logQuery("db.{$table}.remove( :conditions )",compact('conditions'));}$return = true;} catch (MongoException $e) {$this->error = $e->getMessage();trigger_error($this->error);}return $return;}/*** Read Data** For deleteAll(true) calls - the conditions will arrive here as true - account for that and switch to an empty array** @param Model $Model Model Instance* @param array $query Query data* @param mixed $recursive* @return array Results* @access public*/public function read(Model $Model, $query = array(), $recursive = null) {if (!$this->isConnected()) {return false;}$this->_setEmptyValues($query);extract($query);if (!empty($order[0])) {$order = array_shift($order);}$this->_stripAlias($conditions, $Model->alias);$this->_stripAlias($fields, $Model->alias, false, 'value');$this->_stripAlias($order, $Model->alias, false, 'both');if(!empty($conditions['id']) && empty($conditions['_id'])) {$conditions['_id'] = $conditions['id'];unset($conditions['id']);}if (!empty($conditions['_id'])) {$this->_convertId($conditions['_id']);}$fields = (is_array($fields)) ? $fields : array($fields => 1);if ($conditions === true) {$conditions = array();} elseif (!is_array($conditions)) {$conditions = array($conditions);}$order = (is_array($order)) ? $order : array($order);if (is_array($order)) {foreach($order as $field => &$dir) {if (is_numeric($field) || is_null($dir)) {unset ($order[$field]);continue;}if ($dir && strtoupper($dir) === 'ASC') {$dir = 1;continue;} elseif (!$dir || strtoupper($dir) === 'DESC') {$dir = -1;continue;}$dir = (int)$dir;}}if (empty($offset) && $page && $limit) {$offset = ($page - 1) * $limit;}$return = array();$this->_prepareLogQuery($Model); // just sets a timer$table = $this->fullTableName($Model);if (empty($modify)) {if ($Model->findQueryType === 'count' && $fields == array('count' => true)) {$cursor = $this->_db->selectCollection($table)->find($conditions, array('_id' => true));if (!empty($hint)) {$cursor->hint($hint);}$count = $cursor->count();if ($this->fullDebug) {if (empty($hint)) {$hint = array();}$this->logQuery("db.{$table}.find( :conditions ).hint( :hint ).count()",compact('conditions', 'count', 'hint'));}return array(array($Model->alias => array('count' => $count)));}$return = $this->_db->selectCollection($table)->find($conditions, $fields)->sort($order)->limit($limit)->skip($offset);if (!empty($hint)) {$return->hint($hint);}if ($this->fullDebug) {$count = $return->count(true);if (empty($hint)) {$hint = array();}$this->logQuery("db.{$table}.find( :conditions, :fields ).sort( :order ).limit( :limit ).skip( :offset ).hint( :hint )",compact('conditions', 'fields', 'order', 'limit', 'offset', 'count', 'hint'));}} else {$options = array_filter(array('findandmodify' => $table,'query' => $conditions,'sort' => $order,'remove' => !empty($remove),'update' => $this->setMongoUpdateOperator($Model, $modify),'new' => !empty($new),'fields' => $fields,'upsert' => !empty($upsert)));$return = $this->_db->command($options);if ($this->fullDebug) {if ($return['ok']) {$count = 1;if ($this->config['set_string_id'] && !empty($return['value']['_id']) && is_object($return['value']['_id'])) {$return['value']['_id'] = $return['value']['_id']->__toString();}$return[][$Model->alias] = $return['value'];} else {$count = 0;}$this->logQuery("db.runCommand( :options )",array('options' => array_filter($options), 'count' => $count));}}if ($Model->findQueryType === 'count') {return array(array($Model->alias => array('count' => $return->count())));}if (is_object($return)) {$_return = array();while ($return->hasNext()) {$mongodata = $return->getNext();if ($this->config['set_string_id'] && !empty($mongodata['_id']) && is_object($mongodata['_id'])) {$mongodata['_id'] = $mongodata['_id']->__toString();}if ($Model->primaryKey !== '_id') {$mongodata[$Model->primaryKey] = $mongodata['_id'];unset($mongodata['_id']);}$_return[][$Model->alias] = $mongodata;}return $_return;}return $return;}/*** rollback method** MongoDB doesn't support transactions** @return void* @access public*/public function rollback() {return false;}/*** Deletes all the records in a table** @param mixed $table A string or model class representing the table to be truncated* @return boolean* @access public*/public function truncate($table) {if (!$this->isConnected()) {return false;}$fullTableName = $this->fullTableName($table);$return = false;try{$return = $this->getMongoDb()->selectCollection($fullTableName)->remove(array());if ($this->fullDebug) {$this->logQuery("db.{$fullTableName}.remove({})");}$return = true;} catch (MongoException $e) {$this->error = $e->getMessage();trigger_error($this->error);}return $return;}/*** query method* If call getMongoDb() from model, this method call getMongoDb().** @param mixed $query* @param array $params array()* @return void* @access public*/public function query() {$args = func_get_args();$query = $args[0];$params = array();if(count($args) > 1) {$params = $args[1];}if (!$this->isConnected()) {return false;}if($query === 'getMongoDb') {return $this->getMongoDb();}if (count($args) > 1 && (strpos($args[0], 'findBy') === 0 || strpos($args[0], 'findAllBy') === 0)) {$params = $args[1];if (substr($args[0], 0, 6) === 'findBy') {$field = Inflector::underscore(substr($args[0], 6));return $args[2]->find('first', array('conditions' => array($field => $args[1][0])));} else{$field = Inflector::underscore(substr($args[0], 9));return $args[2]->find('all', array('conditions' => array($field => $args[1][0])));}}if(isset($args[2]) && is_a($args[2], 'Model')) {$this->_prepareLogQuery($args[2]);}$return = $this->_db->command($query);if ($this->fullDebug) {$this->logQuery("db.runCommand( :query )", compact('query'));}return $return;}/*** mapReduce** @param mixed $query* @param integer $timeout (milli second)* @return mixed false or array* @access public*/public function mapReduce($query, $timeout = null) {//above MongoDB1.8, query must object.if(isset($query['query']) && !is_object($query['query'])) {$query['query'] = (object)$query['query'];}$result = $this->query($query);if($result['ok']) {if (isset($query['out']['inline']) && $query['out']['inline'] === 1) {if (is_array($result['results'])) {$data = $result['results'];}else{$data = false;}}else {$data = $this->_db->selectCollection($result['result'])->find();if(!empty($timeout)) {$data->timeout($timeout);}}return $data;}return false;}/*** Prepares a value, or an array of values for database queries by quoting and escaping them.** @param mixed $data A value or an array of values to prepare.* @param string $column The column into which this data will be inserted* @return mixed Prepared value or array of values.* @access public*/public function value($data, $column = null) {if (is_array($data) && !empty($data)) {return array_map(array(&$this, 'value'),$data, array_fill(0, count($data), $column));} elseif (is_object($data) && isset($data->type, $data->value)) {if ($data->type == 'identifier') {return $this->name($data->value);} elseif ($data->type == 'expression') {return $data->value;}} elseif (in_array($data, array('{$__cakeID__$}', '{$__cakeForeignKey__$}'), true)) {return $data;}if ($data === null || (is_array($data) && empty($data))) {return 'NULL';}if (empty($column)) {$column = $this->introspectType($data);}switch ($column) {case 'binary':case 'string':case 'text':return $data;case 'boolean':return !empty($data);default:if ($data === '') {return 'NULL';}if (is_float($data)) {return str_replace(',', '.', strval($data));}return $data;}}/*** execute method** If there is no query or the query is true, execute has probably been called as part of a* db-agnostic process which does not have a mongo equivalent, don't do anything.** @param mixed $query* @param array $options* @param array $params array()* @return void* @access public*/public function execute($query, $options = array(), $params = array()) {if (!$this->isConnected()) {return false;}if (!$query || $query === true) {return;}$this->_prepareLogQuery($Model); // just sets a timer$return = $this->_db->execute($query, $params);if ($this->fullDebug) {if ($params) {$this->logQuery(":query, :params",compact('query', 'params'));} else {$this->logQuery($query);}}if ($return['ok']) {return $return['retval'];}return $return;}/*** Set empty values, arrays or integers, for the variables Mongo uses** @param mixed $data* @param array $integers array('limit', 'offset')* @return void* @access protected*/protected function _setEmptyValues(&$data, $integers = array('limit', 'offset')) {if (!is_array($data)) {return;}foreach($data as $key => $value) {if (empty($value)) {if (in_array($key, $integers)) {$data[$key] = 0;} else {$data[$key] = array();}}}}/*** prepareLogQuery method** Any prep work to log a query** @param mixed $Model* @return void* @access protected*/protected function _prepareLogQuery(&$Model) {if (!$this->fullDebug) {return false;}$this->_startTime = microtime(true);$this->took = null;$this->affected = null;$this->error = null;$this->numRows = null;return true;}/*** setTimeout Method** Sets the MongoCursor timeout so long queries (like map / reduce) can run at will.* Expressed in milliseconds, for an infinite timeout, set to -1** @param int $ms* @return boolean* @access public*/public function setTimeout($ms){MongoCursor::$timeout = $ms;return true;}/*** logQuery method** Set timers, errors and refer to the parent* If there are arguments passed - inject them into the query* Show MongoIds in a copy-and-paste-into-mongo format*** @param mixed $query* @param array $args array()* @return void* @access public*/public function logQuery($query, $args = array()) {if ($args) {$this->_stringify($args);$query = String::insert($query, $args);}$this->took = round((microtime(true) - $this->_startTime) * 1000, 0);$this->affected = null;if (empty($this->error['err'])) {$this->error = $this->_db->lastError();if (!is_scalar($this->error)) {$this->error = json_encode($this->error);}}$this->numRows = !empty($args['count'])?$args['count']:null;$query = preg_replace('@"ObjectId\((.*?)\)"@', 'ObjectId ("\1")', $query);return parent::logQuery($query);}/*** convertId method** $conditions is used to determine if it should try to auto correct _id => array() queries* it only appies to conditions, hence the param name** @param mixed $mixed* @param bool $conditions false* @return void* @access protected*/protected function _convertId(&$mixed, $conditions = false) {if (is_int($mixed) || ctype_digit($mixed)) {return;}if (is_string($mixed)) {if (strlen($mixed) !== 24) {return;}$mixed = new MongoId($mixed);}if (is_array($mixed)) {foreach($mixed as &$row) {$this->_convertId($row, false);}if (!empty($mixed[0]) && $conditions) {$mixed = array('$in' => $mixed);}}}/*** stringify method** Takes an array of args as an input and returns an array of json-encoded strings. Takes care of* any objects the arrays might be holding (MongoID);** @param array $args array()* @param int $level 0 internal recursion counter* @return array* @access protected*/protected function _stringify(&$args = array(), $level = 0) {foreach($args as &$arg) {if (is_array($arg)) {$this->_stringify($arg, $level + 1);} elseif (is_object($arg) && is_callable(array($arg, '__toString'))) {$class = get_class($arg);if ($class === 'MongoId') {$arg = 'ObjectId(' . $arg->__toString() . ')';} elseif ($class === 'MongoRegex') {$arg = '_regexstart_' . $arg->__toString() . '_regexend_';} else {$arg = $class . '(' . $arg->__toString() . ')';}}if ($level === 0) {$arg = json_encode($arg);if (strpos($arg, '_regexstart_')) {preg_match_all('@"_regexstart_(.*?)_regexend_"@', $arg, $matches);foreach($matches[0] as $i => $whole) {$replace = stripslashes($matches[1][$i]);$arg = str_replace($whole, $replace, $arg);}}}}}/*** Convert automatically array('Model.field' => 'foo') to array('field' => 'foo')** This introduces the limitation that you can't have a (nested) field with the same name as the model* But it's a small price to pay to be able to use other behaviors/functionality with mongoDB** @param array $args array()* @param string $alias 'Model'* @param bool $recurse true* @param string $check 'key', 'value' or 'both'* @return void* @access protected*/protected function _stripAlias(&$args = array(), $alias = 'Model', $recurse = true, $check = 'key') {if (!is_array($args)) {return;}$checkKey = ($check === 'key' || $check === 'both');$checkValue = ($check === 'value' || $check === 'both');foreach($args as $key => &$val) {if ($checkKey) {if (strpos($key, $alias . '.') === 0) {unset($args[$key]);$key = substr($key, strlen($alias) + 1);$args[$key] = $val;}}if ($checkValue) {if (is_string($val) && strpos($val, $alias . '.') === 0) {$val = substr($val, strlen($alias) + 1);}}if ($recurse && is_array($val)) {$this->_stripAlias($val, $alias, true, $check);}}}}/*** MongoDbDateFormatter method** This function cannot be in the class because of the way model save is written** @param mixed $date null* @return void* @access public*/function MongoDbDateFormatter($date = null) {if ($date) {return new MongoDate($date);}return new MongoDate();}