Skip to content
Fetching contributors…
Cannot retrieve contributors at this time
executable file 1616 lines (1453 sloc) 39.7 KB
<?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 timer
try{
$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 timer
if (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) {
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 timer
if (!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 operator
if(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();
}
Something went wrong with that request. Please try again.