PageRenderTime 58ms CodeModel.GetById 25ms RepoModel.GetById 0ms app.codeStats 1ms

/models/datasources/mongodb_source.php

https://github.com/skie/mongoDB-Datasource
PHP | 1322 lines | 730 code | 122 blank | 470 comment | 154 complexity | 5613f550fe1fa108349e2eed123c00eb MD5 | raw file
  1. <?php
  2. /**
  3. * A CakePHP datasource for the mongoDB (http://www.mongodb.org/) document-oriented database.
  4. *
  5. * This datasource uses Pecl Mongo (http://php.net/mongo)
  6. * and is thus dependent on PHP 5.0 and greater.
  7. *
  8. * Original implementation by ichikaway(Yasushi Ichikawa) http://github.com/ichikaway/
  9. *
  10. * Reference:
  11. * Nate Abele's lithium mongoDB datasource (http://li3.rad-dev.org/)
  12. * Joél Perras' divan(http://github.com/jperras/divan/)
  13. *
  14. * Copyright 2010, Yasushi Ichikawa http://github.com/ichikaway/
  15. *
  16. * Contributors: Predominant, Jrbasso, tkyk, AD7six
  17. *
  18. * Licensed under The MIT License
  19. * Redistributions of files must retain the above copyright notice.
  20. *
  21. * @copyright Copyright 2010, Yasushi Ichikawa http://github.com/ichikaway/
  22. * @package mongodb
  23. * @subpackage mongodb.models.datasources
  24. * @license http://www.opensource.org/licenses/mit-license.php The MIT License
  25. */
  26. App::import('Datasource', 'DboSource');
  27. /**
  28. * MongoDB Source
  29. *
  30. * @package mongodb
  31. * @subpackage mongodb.models.datasources
  32. */
  33. class MongodbSource extends DboSource {
  34. /**
  35. * Are we connected to the DataSource?
  36. *
  37. * true - yes
  38. * null - haven't tried yet
  39. * false - nope, and we can't connect
  40. *
  41. * @var boolean
  42. * @access public
  43. */
  44. public $connected = null;
  45. /**
  46. * Database Instance
  47. *
  48. * @var resource
  49. * @access protected
  50. */
  51. protected $_db = null;
  52. /**
  53. * Mongo Driver Version
  54. *
  55. * @var string
  56. * @access protected
  57. */
  58. protected $_driverVersion = Mongo::VERSION;
  59. /**
  60. * startTime property
  61. *
  62. * If debugging is enabled, stores the (micro)time the current query started
  63. *
  64. * @var mixed null
  65. * @access protected
  66. */
  67. protected $_startTime = null;
  68. /**
  69. * Base Config
  70. *
  71. * set_string_id:
  72. * true: In read() method, convert MongoId object to string and set it to array 'id'.
  73. * false: not convert and set.
  74. *
  75. * @var array
  76. * @access public
  77. *
  78. */
  79. public $_baseConfig = array(
  80. 'set_string_id' => true,
  81. 'persistent' => false,
  82. 'host' => 'localhost',
  83. 'database' => '',
  84. 'port' => '27017',
  85. 'login' => '',
  86. 'password' => ''
  87. );
  88. /**
  89. * column definition
  90. *
  91. * @var array
  92. */
  93. public $columns = array(
  94. 'boolean' => array('name' => 'boolean'),
  95. 'string' => array('name' => 'varchar'),
  96. 'text' => array('name' => 'text'),
  97. 'integer' => array('name' => 'integer', 'format' => null, 'formatter' => 'intval'),
  98. 'float' => array('name' => 'float', 'format' => null, 'formatter' => 'floatval'),
  99. 'datetime' => array('name' => 'datetime', 'format' => null, 'formatter' => 'MongodbDateFormatter'),
  100. 'timestamp' => array('name' => 'timestamp', 'format' => null, 'formatter' => 'MongodbDateFormatter'),
  101. 'time' => array('name' => 'time', 'format' => null, 'formatter' => 'MongodbDateFormatter'),
  102. 'date' => array('name' => 'date', 'format' => null, 'formatter' => 'MongodbDateFormatter'),
  103. );
  104. /**
  105. * Default schema for the mongo models
  106. *
  107. * @var array
  108. * @access protected
  109. */
  110. protected $_defaultSchema = array(
  111. '_id' => array('type' => 'string', 'length' => 24, 'key' => 'primary'),
  112. 'created' => array('type' => 'datetime', 'default' => null)
  113. );
  114. /**
  115. * construct method
  116. *
  117. * By default don't try to connect until you need to
  118. *
  119. * @param array $config Configuration array
  120. * @param bool $autoConnect false
  121. * @return void
  122. * @access public
  123. */
  124. function __construct($config = array(), $autoConnect = false) {
  125. return parent::__construct($config, $autoConnect);
  126. }
  127. /**
  128. * Destruct
  129. *
  130. * @access public
  131. */
  132. public function __destruct() {
  133. if ($this->connected) {
  134. $this->disconnect();
  135. }
  136. }
  137. /**
  138. * commit method
  139. *
  140. * MongoDB doesn't support transactions
  141. *
  142. * @return void
  143. * @access public
  144. */
  145. public function commit() {
  146. return false;
  147. }
  148. /**
  149. * Connect to the database
  150. *
  151. * If using 1.0.2 or above use the mongodb:// format to connect
  152. * The connect syntax changed in version 1.0.2 - so check for that too
  153. *
  154. * If authentication information in present then authenticate the connection
  155. *
  156. * @return boolean Connected
  157. * @access public
  158. */
  159. public function connect() {
  160. $this->connected = false;
  161. try{
  162. if (false && $this->_driverVersion >= '1.0.2' && $this->config['host'] != 'localhost') {
  163. $host = "mongodb://";
  164. } else {
  165. $host = '';
  166. }
  167. $host .= $this->config['host'] . ':' . $this->config['port'];
  168. if (false && $this->_driverVersion >= '1.0.2') {
  169. $this->connection = new Mongo($host, array("persist" => $this->config['persistent']));
  170. } else {
  171. $this->connection = new Mongo($host, true, $this->config['persistent']);
  172. }
  173. if ($this->_db = $this->connection->selectDB($this->config['database'])) {
  174. if (!empty($this->config['login'])) {
  175. $return = $this->_db->authenticate($this->config['login'], $this->config['password']);
  176. if (!$return || !$return['ok']) {
  177. trigger_error('MongodbSource::connect ' . $return['errmsg']);
  178. return false;
  179. }
  180. }
  181. $this->connected = true;
  182. }
  183. } catch(MongoException $e) {
  184. $this->error = $e->getMessage();
  185. trigger_error($this->error);
  186. }
  187. return $this->connected;
  188. }
  189. /**
  190. * Inserts multiple values into a table
  191. *
  192. * @param string $table
  193. * @param string $fields
  194. * @param array $values
  195. * @access public
  196. */
  197. public function insertMulti($table, $fields, $values) {
  198. $table = $this->fullTableName($table);
  199. if (!is_array($fields) || !is_array($values)) {
  200. return false;
  201. }
  202. $data = array();
  203. foreach($values as $row) {
  204. if (is_string($row)) {
  205. $row = explode(', ', substr($row, 1, -1));
  206. }
  207. $data[] = array_combine($fields, $row);
  208. }
  209. $this->_prepareLogQuery($table); // just sets a timer
  210. try{
  211. $return = $this->_db
  212. ->selectCollection($table)
  213. ->batchInsert($data, array('safe' => true));
  214. } catch (MongoException $e) {
  215. $this->error = $e->getMessage();
  216. trigger_error($this->error);
  217. }
  218. if ($this->fullDebug) {
  219. $this->logQuery("db.{$table}.insertMulti( :data , array('safe' => true))", compact('data'));
  220. }
  221. }
  222. /**
  223. * check connection to the database
  224. *
  225. * @return boolean Connected
  226. * @access public
  227. */
  228. public function isConnected() {
  229. if ($this->connected === false) {
  230. return false;
  231. }
  232. return $this->connect();
  233. }
  234. /**
  235. * get MongoDB Object
  236. *
  237. * @return mixed MongoDB Object
  238. * @access public
  239. */
  240. public function getMongoDb() {
  241. if ($this->connected === false) {
  242. return false;
  243. }
  244. return $this->_db;
  245. }
  246. /**
  247. * get MongoDB Collection Object
  248. *
  249. * @return mixed MongoDB Collection Object
  250. * @access public
  251. */
  252. public function getMongoCollection(&$Model) {
  253. if ($this->connected === false) {
  254. return false;
  255. }
  256. $collection = $this->_db
  257. ->selectCollection($Model->table);
  258. return $collection;
  259. }
  260. /**
  261. * isInterfaceSupported method
  262. *
  263. * listSources is infact supported, however: cake expects it to return a complete list of all
  264. * possible sources in the selected db - the possible list of collections is infinte, so it's
  265. * faster and simpler to tell cake that the interface is /not/ supported so it assumes that
  266. * <insert name of your table here> exist
  267. *
  268. * @param mixed $interface
  269. * @return void
  270. * @access public
  271. */
  272. public function isInterfaceSupported($interface) {
  273. if ($interface === 'listSources') {
  274. return false;
  275. }
  276. return parent::isInterfaceSupported($interface);
  277. }
  278. /**
  279. * Close database connection
  280. *
  281. * @return boolean Connected
  282. * @access public
  283. */
  284. public function close() {
  285. return $this->disconnect();
  286. }
  287. /**
  288. * Disconnect from the database
  289. *
  290. * @return boolean Connected
  291. * @access public
  292. */
  293. public function disconnect() {
  294. if ($this->connected) {
  295. $this->connected = !$this->connection->close();
  296. unset($this->_db, $this->connection);
  297. return !$this->connected;
  298. }
  299. return true;
  300. }
  301. /**
  302. * Get list of available Collections
  303. *
  304. * @param array $data
  305. * @return array Collections
  306. * @access public
  307. */
  308. public function listSources($data = null) {
  309. if (!$this->isConnected()) {
  310. return false;
  311. }
  312. $collections = $this->_db->listCollections();
  313. $sources = array();
  314. if(!empty($collections)){
  315. foreach($collections as $collection){
  316. $sources[] = $collection->getName();
  317. }
  318. }
  319. return $sources;
  320. }
  321. /**
  322. * Describe
  323. *
  324. * Automatically bind the schemaless behavior if there is no explicit mongo schema.
  325. * When called, if there is model data it will be used to derive a schema. a row is plucked
  326. * out of the db and the data obtained used to derive the schema.
  327. *
  328. * @param Model $Model
  329. * @return array if model instance has mongoSchema, return it.
  330. * @access public
  331. */
  332. public function describe(&$Model, $field = null) {
  333. $Model->primaryKey = '_id';
  334. $schema = array();
  335. if (!empty($Model->mongoSchema) && is_array($Model->mongoSchema)) {
  336. $schema = $Model->mongoSchema;
  337. return $schema + $this->_defaultSchema;
  338. } elseif ($this->isConnected() && is_a($Model, 'Model') && !empty($Model->Behaviors)) {
  339. $Model->Behaviors->attach('Mongodb.Schemaless');
  340. if (!$Model->data) {
  341. if ($this->_db->selectCollection($Model->table)->count()) {
  342. return $this->deriveSchemaFromData($Model, $this->_db->selectCollection($Model->table)->findOne());
  343. }
  344. }
  345. }
  346. return $this->deriveSchemaFromData($Model);
  347. }
  348. /**
  349. * begin method
  350. *
  351. * Mongo doesn't support transactions
  352. *
  353. * @return void
  354. * @access public
  355. */
  356. public function begin() {
  357. return false;
  358. }
  359. /**
  360. * Calculate
  361. *
  362. * @param Model $Model
  363. * @return array
  364. * @access public
  365. */
  366. public function calculate(&$Model) {
  367. return array('count' => true);
  368. }
  369. /**
  370. * Quotes identifiers.
  371. *
  372. * MongoDb does not need identifiers quoted, so this method simply returns the identifier.
  373. *
  374. * @param string $name The identifier to quote.
  375. * @return string The quoted identifier.
  376. */
  377. public function name($name) {
  378. return $name;
  379. }
  380. /**
  381. * Create Data
  382. *
  383. * @param Model $Model Model Instance
  384. * @param array $fields Field data
  385. * @param array $values Save data
  386. * @return boolean Insert result
  387. * @access public
  388. */
  389. public function create(&$Model, $fields = null, $values = null) {
  390. if (!$this->isConnected()) {
  391. return false;
  392. }
  393. if ($fields !== null && $values !== null) {
  394. $data = array_combine($fields, $values);
  395. } else {
  396. $data = $Model->data;
  397. }
  398. if (!empty($data['_id'])) {
  399. $this->_convertId($data['_id']);
  400. }
  401. $this->_prepareLogQuery($Model); // just sets a timer
  402. try{
  403. $return = $this->_db
  404. ->selectCollection($Model->table)
  405. ->insert($data, true);
  406. } catch (MongoException $e) {
  407. $this->error = $e->getMessage();
  408. trigger_error($this->error);
  409. }
  410. if ($this->fullDebug) {
  411. $this->logQuery("db.{$Model->useTable}.insert( :data , true)", compact('data'));
  412. }
  413. if (!empty($return) && $return['ok']) {
  414. $id = is_object($data['_id']) ? $data['_id']->__toString() : null;
  415. $Model->setInsertID($id);
  416. $Model->id = $id;
  417. return true;
  418. }
  419. return false;
  420. }
  421. /**
  422. * createSchema method
  423. *
  424. * Mongo no care for creating schema. Mongo work with no schema.
  425. *
  426. * @param mixed $schema
  427. * @param mixed $tableName null
  428. * @return void
  429. * @access public
  430. */
  431. public function createSchema($schema, $tableName = null) {
  432. return true;
  433. }
  434. /**
  435. * dropSchema method
  436. *
  437. * Return a command to drop each table
  438. *
  439. * @param mixed $schema
  440. * @param mixed $tableName null
  441. * @return void
  442. * @access public
  443. */
  444. public function dropSchema($schema, $tableName = null) {
  445. if (!$this->isConnected()) {
  446. return false;
  447. }
  448. if (!is_a($schema, 'CakeSchema')) {
  449. trigger_error(__('Invalid schema object', true), E_USER_WARNING);
  450. return null;
  451. }
  452. if ($tableName) {
  453. return "db.{$tableName}.drop();";
  454. }
  455. $toDrop = array();
  456. foreach ($schema->tables as $curTable => $columns) {
  457. if ($tableName === $curTable) {
  458. $toDrop[] = $curTable;
  459. }
  460. }
  461. if (count($toDrop) === 1) {
  462. return "db.{$toDrop[0]}.drop();";
  463. }
  464. $return = "toDrop = :tables;\nfor( i = 0; i < toDrop.length; i++ ) {\n\tdb[toDrop[i]].drop();\n}";
  465. $tables = '["' . implode($toDrop, '", "') . '"]';
  466. return String::insert($return, compact('tables'));
  467. }
  468. /**
  469. * distinct method
  470. *
  471. * @param mixed $Model
  472. * @param array $keys array()
  473. * @param array $params array()
  474. * @return void
  475. * @access public
  476. */
  477. public function distinct(&$Model, $keys = array(), $params = array()) {
  478. if (!$this->isConnected()) {
  479. return false;
  480. }
  481. $this->_prepareLogQuery($Model); // just sets a timer
  482. if (array_key_exists('conditions', $params)) {
  483. $params = $params['conditions'];
  484. }
  485. try{
  486. $return = $this->_db
  487. ->selectCollection($Model->table)
  488. ->distinct($keys, $params);
  489. } catch (MongoException $e) {
  490. $this->error = $e->getMessage();
  491. trigger_error($this->error);
  492. }
  493. if ($this->fullDebug) {
  494. $this->logQuery("db.{$Model->useTable}.distinct( :keys, :params )", compact('keys', 'params'));
  495. }
  496. return $return;
  497. }
  498. /**
  499. * group method
  500. *
  501. * @param mixed $Model
  502. * @param array $params array()
  503. * Set params same as MongoCollection::group()
  504. * key,initial, reduce, options(conditions, finalize)
  505. *
  506. * Ex. $params = array(
  507. * 'key' => array('field' => true),
  508. * 'initial' => array('csum' => 0),
  509. * 'reduce' => 'function(obj, prev){prev.csum += 1;}',
  510. * 'options' => array(
  511. * 'condition' => array('age' => array('$gt' => 20)),
  512. * 'finalize' => array(),
  513. * ),
  514. * );
  515. * @return void
  516. * @access public
  517. */
  518. public function group(&$Model, $params = array()) {
  519. if (!$this->isConnected() || count($params) === 0 ) {
  520. return false;
  521. }
  522. $this->_prepareLogQuery($Model); // just sets a timer
  523. $key = (empty($params['key'])) ? array() : $params['key'];
  524. $initial = (empty($params['initial'])) ? array() : $params['initial'];
  525. $reduce = (empty($params['reduce'])) ? array() : $params['reduce'];
  526. $options = (empty($params['options'])) ? array() : $params['options'];
  527. try{
  528. $return = $this->_db
  529. ->selectCollection($Model->table)
  530. ->group($key, $initial, $reduce, $options);
  531. } catch (MongoException $e) {
  532. $this->error = $e->getMessage();
  533. trigger_error($this->error);
  534. }
  535. if ($this->fullDebug) {
  536. $this->logQuery("db.{$Model->useTable}.group( :key, :initial, :reduce, :options )", $params);
  537. }
  538. return $return;
  539. }
  540. /**
  541. * ensureIndex method
  542. *
  543. * @param mixed $Model
  544. * @param array $keys array()
  545. * @param array $params array()
  546. * @return void
  547. * @access public
  548. */
  549. public function ensureIndex(&$Model, $keys = array(), $params = array()) {
  550. if (!$this->isConnected()) {
  551. return false;
  552. }
  553. $this->_prepareLogQuery($Model); // just sets a timer
  554. try{
  555. $return = $this->_db
  556. ->selectCollection($Model->table)
  557. ->ensureIndex($keys, $params);
  558. } catch (MongoException $e) {
  559. $this->error = $e->getMessage();
  560. trigger_error($this->error);
  561. }
  562. if ($this->fullDebug) {
  563. $this->logQuery("db.{$Model->useTable}.ensureIndex( :keys, :params )", compact('keys', 'params'));
  564. }
  565. return $return;
  566. }
  567. /**
  568. * Update Data
  569. *
  570. * This method uses $set operator automatically with MongoCollection::update().
  571. * If you don't want to use $set operator, you can chose any one as follw.
  572. * 1. Set TRUE in Model::mongoNoSetOperator property.
  573. * 2. Set a mongodb operator in a key of save data as follow.
  574. * Model->save(array('_id' => $id, '$inc' => array('count' => 1)));
  575. * Don't use Model::mongoSchema property,
  576. * CakePHP delete '$inc' data in Model::Save().
  577. * 3. Set a Mongo operator in Model::mongoNoSetOperator property.
  578. * Model->mongoNoSetOperator = '$inc';
  579. * Model->save(array('_id' => $id, array('count' => 1)));
  580. *
  581. * @param Model $Model Model Instance
  582. * @param array $fields Field data
  583. * @param array $values Save data
  584. * @return boolean Update result
  585. * @access public
  586. */
  587. public function update(&$Model, $fields = null, $values = null, $conditions = null) {
  588. if (!$this->isConnected()) {
  589. return false;
  590. }
  591. if ($fields !== null && $values !== null) {
  592. $data = array_combine($fields, $values);
  593. } elseif($fields !== null && $conditions !== null) {
  594. return $this->updateAll($Model, $fields, $conditions);
  595. } else{
  596. $data = $Model->data;
  597. }
  598. if (empty($data['_id'])) {
  599. $data['_id'] = $Model->id;
  600. }
  601. $this->_convertId($data['_id']);
  602. try{
  603. $mongoCollectionObj = $this->_db
  604. ->selectCollection($Model->table);
  605. } catch (MongoException $e) {
  606. $this->error = $e->getMessage();
  607. trigger_error($this->error);
  608. return false;
  609. }
  610. $this->_prepareLogQuery($Model); // just sets a timer
  611. if (!empty($data['_id'])) {
  612. $this->_convertId($data['_id']);
  613. $cond = array('_id' => $data['_id']);
  614. unset($data['_id']);
  615. //setting Mongo operator
  616. if(empty($Model->mongoNoSetOperator)) {
  617. $keys = array_keys($data);
  618. if(substr($keys[0],0,1) !== '$') {
  619. $data = array('$set' => $data);
  620. }
  621. } elseif(substr($Model->mongoNoSetOperator,0,1) === '$') {
  622. if(!empty($data['modified'])) {
  623. $modified = $data['modified'];
  624. unset($data['modified']);
  625. $data = array($Model->mongoNoSetOperator => $data, '$set' => array('modified' => $modified));
  626. } else {
  627. $data = array($Model->mongoNoSetOperator => $data);
  628. }
  629. }
  630. try{
  631. $return = $mongoCollectionObj->update($cond, $data, array("multiple" => false));
  632. } catch (MongoException $e) {
  633. $this->error = $e->getMessage();
  634. trigger_error($this->error);
  635. }
  636. if ($this->fullDebug) {
  637. $this->logQuery("db.{$Model->useTable}.update( :conditions, :data, :params )",
  638. array('conditions' => $cond, 'data' => $data, 'params' => array("multiple" => false))
  639. );
  640. }
  641. } else {
  642. try{
  643. $return = $mongoCollectionObj->save($data);
  644. } catch (MongoException $e) {
  645. $this->error = $e->getMessage();
  646. trigger_error($this->error);
  647. }
  648. if ($this->fullDebug) {
  649. $this->logQuery("db.{$Model->useTable}.save( :data )", compact('data'));
  650. }
  651. }
  652. return $return;
  653. }
  654. /**
  655. * Update multiple Record
  656. *
  657. * @param Model $Model Model Instance
  658. * @param array $fields Field data
  659. * @param array $conditions
  660. * @return boolean Update result
  661. * @access public
  662. */
  663. public function updateAll(&$Model, $fields = null, $conditions = null) {
  664. if (!$this->isConnected()) {
  665. return false;
  666. }
  667. $fields = array('$set' => $fields);
  668. $this->_stripAlias($conditions, $Model->alias);
  669. $this->_stripAlias($fields, $Model->alias, false, 'value');
  670. $this->_prepareLogQuery($Model); // just sets a timer
  671. try{
  672. $return = $this->_db
  673. ->selectCollection($Model->table)
  674. ->update($conditions, $fields, array("multiple" => true));
  675. } catch (MongoException $e) {
  676. $this->error = $e->getMessage();
  677. trigger_error($this->error);
  678. }
  679. if ($this->fullDebug) {
  680. $this->logQuery("db.{$Model->useTable}.update( :fields, :params )",
  681. array('fields' => $fields, 'params' => array("multiple" => true))
  682. );
  683. }
  684. return $return;
  685. }
  686. /**
  687. * deriveSchemaFromData method
  688. *
  689. * @param mixed $Model
  690. * @param array $data array()
  691. * @return void
  692. * @access public
  693. */
  694. public function deriveSchemaFromData($Model, $data = array()) {
  695. if (!$data) {
  696. $data = $Model->data;
  697. if ($data && array_key_exists($Model->alias, $data)) {
  698. $data = $data[$Model->alias];
  699. }
  700. }
  701. $return = $this->_defaultSchema;
  702. if ($data) {
  703. $fields = array_keys($data);
  704. foreach($fields as $field) {
  705. if (in_array($field, array('created', 'modified', 'updated'))) {
  706. $return[$field] = array('type' => 'datetime', 'null' => true);
  707. } else {
  708. $return[$field] = array('type' => 'string', 'length' => 2000);
  709. }
  710. }
  711. }
  712. return $return;
  713. }
  714. /**
  715. * Delete Data
  716. *
  717. * For deleteAll(true, false) calls - conditions will arrive here as true - account for that and
  718. * convert to an empty array
  719. * For deleteAll(array('some conditions')) calls - conditions will arrive here as:
  720. * array(
  721. * Alias._id => array(1, 2, 3, ...)
  722. * )
  723. *
  724. * This format won't be understood by mongodb, it'll find 0 rows. convert to:
  725. *
  726. * array(
  727. * Alias._id => array('$in' => array(1, 2, 3, ...))
  728. * )
  729. *
  730. * @TODO bench remove() v drop. if it's faster to drop - just drop the collection taking into
  731. * account existing indexes (recreate just the indexes)
  732. * @param Model $Model Model Instance
  733. * @param array $conditions
  734. * @return boolean Update result
  735. * @access public
  736. */
  737. public function delete(&$Model, $conditions = null) {
  738. if (!$this->isConnected()) {
  739. return false;
  740. }
  741. $id = null;
  742. $this->_stripAlias($conditions, $Model->alias);
  743. if ($conditions === true) {
  744. $conditions = array();
  745. } elseif (empty($conditions)) {
  746. $id = $Model->id;
  747. } elseif (!empty($conditions) && !is_array($conditions)) {
  748. $id = $conditions;
  749. $conditions = array();
  750. }
  751. $mongoCollectionObj = $this->_db
  752. ->selectCollection($Model->table);
  753. $this->_stripAlias($conditions, $Model->alias);
  754. if (!empty($id)) {
  755. $conditions['_id'] = $id;
  756. }
  757. if (!empty($conditions['_id'])) {
  758. $this->_convertId($conditions['_id'], true);
  759. }
  760. $return = false;
  761. $r = false;
  762. try{
  763. $this->_prepareLogQuery($Model); // just sets a timer
  764. $return = $mongoCollectionObj->remove($conditions);
  765. if ($this->fullDebug) {
  766. $this->logQuery("db.{$Model->useTable}.remove( :conditions )",
  767. compact('conditions')
  768. );
  769. }
  770. $return = true;
  771. } catch (MongoException $e) {
  772. $this->error = $e->getMessage();
  773. trigger_error($this->error);
  774. }
  775. return $return;
  776. }
  777. /**
  778. * Read Data
  779. *
  780. * For deleteAll(true) calls - the conditions will arrive here as true - account for that and switch to an empty array
  781. *
  782. * @param Model $Model Model Instance
  783. * @param array $query Query data
  784. * @return array Results
  785. * @access public
  786. */
  787. public function read(&$Model, $query = array()) {
  788. if (!$this->isConnected()) {
  789. return false;
  790. }
  791. $this->_setEmptyValues($query);
  792. extract($query);
  793. if (!empty($order[0])) {
  794. $order = array_shift($order);
  795. }
  796. $this->_stripAlias($conditions, $Model->alias);
  797. $this->_stripAlias($fields, $Model->alias, false, 'value');
  798. $this->_stripAlias($order, $Model->alias, false, 'both');
  799. if (!empty($conditions['_id'])) {
  800. $this->_convertId($conditions['_id']);
  801. }
  802. $fields = (is_array($fields)) ? $fields : array($fields => 1);
  803. if ($conditions === true) {
  804. $conditions = array();
  805. } elseif (!is_array($conditions)) {
  806. $conditions = array($conditions);
  807. }
  808. $order = (is_array($order)) ? $order : array($order);
  809. if (is_array($order)) {
  810. foreach($order as $field => &$dir) {
  811. if (is_numeric($field) || is_null($dir)) {
  812. unset ($order[$field]);
  813. continue;
  814. }
  815. if ($dir && strtoupper($dir) === 'ASC') {
  816. $dir = 1;
  817. continue;
  818. } elseif (!$dir || strtoupper($dir) === 'DESC') {
  819. $dir = -1;
  820. continue;
  821. }
  822. $dir = (int)$dir;
  823. }
  824. }
  825. if (empty($offset) && $page && $limit) {
  826. $offset = ($page - 1) * $limit;
  827. }
  828. $return = array();
  829. $this->_prepareLogQuery($Model); // just sets a timer
  830. if (empty($modify)) {
  831. if ($Model->findQueryType === 'count' && $fields == array('count' => true)) {
  832. $count = $this->_db
  833. ->selectCollection($Model->table)
  834. ->count($conditions);
  835. if ($this->fullDebug) {
  836. $this->logQuery("db.{$Model->useTable}.count( :conditions )",
  837. compact('conditions', 'count')
  838. );
  839. }
  840. return array(array($Model->alias => array('count' => $count)));
  841. }
  842. $return = $this->_db
  843. ->selectCollection($Model->table)
  844. ->find($conditions, $fields)
  845. ->sort($order)
  846. ->limit($limit)
  847. ->skip($offset);
  848. if ($this->fullDebug) {
  849. $count = $return->count();
  850. $this->logQuery("db.{$Model->useTable}.find( :conditions, :fields ).sort( :order ).limit( :limit ).skip( :offset )",
  851. compact('conditions', 'fields', 'order', 'limit', 'offset', 'count')
  852. );
  853. }
  854. } else {
  855. $options = array_filter(array(
  856. 'findandmodify' => $Model->table,
  857. 'query' => $conditions,
  858. 'sort' => $order,
  859. 'remove' => !empty($remove),
  860. 'update' => array('$set' => $modify),
  861. 'new' => !empty($new),
  862. 'fields' => $fields,
  863. 'upsert' => !empty($upsert)
  864. ));
  865. $return = $this->_db
  866. ->command($options);
  867. if ($this->fullDebug) {
  868. if ($return['ok']) {
  869. $count = 1;
  870. if ($this->config['set_string_id'] && !empty($return['value']['_id']) && is_object($return['value']['_id'])) {
  871. $return['value']['_id'] = $return['value']['_id']->__toString();
  872. }
  873. $return[][$Model->alias] = $return['value'];
  874. } else {
  875. $count = 0;
  876. }
  877. $this->logQuery("db.runCommand( :options )",
  878. array('options' => array_filter($options), 'count' => $count)
  879. );
  880. }
  881. }
  882. if ($Model->findQueryType === 'count') {
  883. return array(array($Model->alias => array('count' => $return->count())));
  884. }
  885. if (is_object($return)) {
  886. $_return = array();
  887. while ($return->hasNext()) {
  888. $mongodata = $return->getNext();
  889. if ($this->config['set_string_id'] && !empty($mongodata['_id']) && is_object($mongodata['_id'])) {
  890. $mongodata['_id'] = $mongodata['_id']->__toString();
  891. }
  892. $_return[][$Model->alias] = $mongodata;
  893. }
  894. return $_return;
  895. }
  896. return $return;
  897. }
  898. /**
  899. * rollback method
  900. *
  901. * MongoDB doesn't support transactions
  902. *
  903. * @return void
  904. * @access public
  905. */
  906. public function rollback() {
  907. return false;
  908. }
  909. /**
  910. * Deletes all the records in a table
  911. *
  912. * @param mixed $table A string or model class representing the table to be truncated
  913. * @return boolean
  914. * @access public
  915. */
  916. public function truncate($table) {
  917. if (!$this->isConnected()) {
  918. return false;
  919. }
  920. return $this->execute('db.' . $this->fullTableName($table) . '.remove();');
  921. }
  922. /**
  923. * query method
  924. * If call getMongoDb() from model, this method call getMongoDb().
  925. *
  926. * @param mixed $query
  927. * @param array $params array()
  928. * @return void
  929. * @access public
  930. */
  931. public function query($query, $params = array()) {
  932. if (!$this->isConnected()) {
  933. return false;
  934. }
  935. if($query === 'getMongoDb') {
  936. return $this->getMongoDb();
  937. }
  938. $this->_prepareLogQuery($Model); // just sets a timer
  939. $return = $this->_db
  940. ->command($query);
  941. if ($this->fullDebug) {
  942. $this->logQuery("db.runCommand( :query )", compact('query'));
  943. }
  944. return $return;
  945. }
  946. /**
  947. * getMapReduceResults
  948. *
  949. * @param mixed $query
  950. * @return void
  951. * @access public
  952. */
  953. public function getMapReduceResults($query) {
  954. $result = $this->query($query);
  955. if($result['ok']) {
  956. $data = $this->_db->selectCollection($result['result'])->find();
  957. return $data;
  958. }
  959. return false;
  960. }
  961. /**
  962. * Prepares a value, or an array of values for database queries by quoting and escaping them.
  963. *
  964. * @param mixed $data A value or an array of values to prepare.
  965. * @param string $column The column into which this data will be inserted
  966. * @param boolean $read Value to be used in READ or WRITE context
  967. * @return mixed Prepared value or array of values.
  968. * @access public
  969. */
  970. public function value($data, $column = null, $read = true) {
  971. $return = parent::value($data, $column, $read);
  972. if ($return === null && $data !== null) {
  973. return $data;
  974. }
  975. return $return;
  976. }
  977. /**
  978. * execute method
  979. *
  980. * If there is no query or the query is true, execute has probably been called as part of a
  981. * db-agnostic process which does not have a mongo equivalent, don't do anything.
  982. *
  983. * @param mixed $query
  984. * @param array $params array()
  985. * @return void
  986. * @access public
  987. */
  988. public function execute($query, $params = array()) {
  989. if (!$this->isConnected()) {
  990. return false;
  991. }
  992. if (!$query || $query === true) {
  993. return;
  994. }
  995. $this->_prepareLogQuery($Model); // just sets a timer
  996. $return = $this->_db
  997. ->execute($query, $params);
  998. if ($this->fullDebug) {
  999. if ($params) {
  1000. $this->logQuery(":query, :params",
  1001. compact('query', 'params')
  1002. );
  1003. } else {
  1004. $this->logQuery($query);
  1005. }
  1006. }
  1007. if ($return['ok']) {
  1008. return $return['retval'];
  1009. }
  1010. return $return;
  1011. }
  1012. /**
  1013. * Set empty values, arrays or integers, for the variables Mongo uses
  1014. *
  1015. * @param mixed $data
  1016. * @param array $integers array('limit', 'offset')
  1017. * @return void
  1018. * @access protected
  1019. */
  1020. protected function _setEmptyValues(&$data, $integers = array('limit', 'offset')) {
  1021. if (!is_array($data)) {
  1022. return;
  1023. }
  1024. foreach($data as $key => $value) {
  1025. if (empty($value)) {
  1026. if (in_array($key, $integers)) {
  1027. $data[$key] = 0;
  1028. } else {
  1029. $data[$key] = array();
  1030. }
  1031. }
  1032. }
  1033. }
  1034. /**
  1035. * prepareLogQuery method
  1036. *
  1037. * Any prep work to log a query
  1038. *
  1039. * @param mixed $Model
  1040. * @return void
  1041. * @access protected
  1042. */
  1043. protected function _prepareLogQuery(&$Model) {
  1044. if (!$this->fullDebug) {
  1045. return false;
  1046. }
  1047. $this->_startTime = microtime(true);
  1048. $this->took = null;
  1049. $this->affected = null;
  1050. $this->error = null;
  1051. $this->numRows = null;
  1052. return true;
  1053. }
  1054. /**
  1055. * logQuery method
  1056. *
  1057. * Set timers, errors and refer to the parent
  1058. * If there are arguments passed - inject them into the query
  1059. * Show MongoIds in a copy-and-paste-into-mongo format
  1060. *
  1061. *
  1062. * @param mixed $query
  1063. * @param array $args array()
  1064. * @return void
  1065. * @access public
  1066. */
  1067. public function logQuery($query, $args = array()) {
  1068. if ($args) {
  1069. $this->_stringify($args);
  1070. $query = String::insert($query, $args);
  1071. }
  1072. $this->took = round((microtime(true) - $this->_startTime) * 1000, 0);
  1073. $this->affected = null;
  1074. if (empty($this->error['err'])) {
  1075. $this->error = $this->_db->lastError();
  1076. if (!is_scalar($this->error)) {
  1077. $this->error = json_encode($this->error);
  1078. }
  1079. }
  1080. $this->numRows = !empty($args['count'])?$args['count']:null;
  1081. $query = preg_replace('@"ObjectId\((.*?)\)"@', 'ObjectId ("\1")', $query);
  1082. return parent::logQuery($query);
  1083. }
  1084. /**
  1085. * convertId method
  1086. *
  1087. * $conditions is used to determine if it should try to auto correct _id => array() queries
  1088. * it only appies to conditions, hence the param name
  1089. *
  1090. * @param mixed $mixed
  1091. * @param bool $conditions false
  1092. * @return void
  1093. * @access protected
  1094. */
  1095. protected function _convertId(&$mixed, $conditions = false) {
  1096. if (is_string($mixed)) {
  1097. if (strlen($mixed) !== 24) {
  1098. return;
  1099. }
  1100. $mixed = new MongoId($mixed);
  1101. }
  1102. if (is_array($mixed)) {
  1103. foreach($mixed as &$row) {
  1104. $this->_convertId($row, false);
  1105. }
  1106. if (!empty($mixed[0]) && $conditions) {
  1107. $mixed = array('$in' => $mixed);
  1108. }
  1109. }
  1110. }
  1111. /**
  1112. * stringify method
  1113. *
  1114. * Takes an array of args as an input and returns an array of json-encoded strings. Takes care of
  1115. * any objects the arrays might be holding (MongoID);
  1116. *
  1117. * @param array $args array()
  1118. * @param int $level 0 internal recursion counter
  1119. * @return array
  1120. * @access protected
  1121. */
  1122. protected function _stringify(&$args = array(), $level = 0) {
  1123. foreach($args as &$arg) {
  1124. if (is_array($arg)) {
  1125. $this->_stringify($arg, $level + 1);
  1126. } elseif (is_object($arg) && is_callable(array($arg, '__toString'))) {
  1127. $class = get_class($arg);
  1128. if ($class === 'MongoId') {
  1129. $arg = 'ObjectId(' . $arg->__toString() . ')';
  1130. } elseif ($class === 'MongoRegex') {
  1131. $arg = '_regexstart_' . $arg->__toString() . '_regexend_';
  1132. } else {
  1133. $arg = $class . '(' . $arg->__toString() . ')';
  1134. }
  1135. }
  1136. if ($level === 0) {
  1137. $arg = json_encode($arg);
  1138. if (strpos($arg, '_regexstart_')) {
  1139. preg_match_all('@"_regexstart_(.*?)_regexend_"@', $arg, $matches);
  1140. foreach($matches[0] as $i => $whole) {
  1141. $replace = stripslashes($matches[1][$i]);
  1142. $arg = str_replace($whole, $replace, $arg);
  1143. }
  1144. }
  1145. }
  1146. }
  1147. }
  1148. /**
  1149. * Convert automatically array('Model.field' => 'foo') to array('field' => 'foo')
  1150. *
  1151. * This introduces the limitation that you can't have a (nested) field with the same name as the model
  1152. * But it's a small price to pay to be able to use other behaviors/functionality with mongoDB
  1153. *
  1154. * @param array $args array()
  1155. * @param string $alias 'Model'
  1156. * @param bool $recurse true
  1157. * @param string $check 'key', 'value' or 'both'
  1158. * @return void
  1159. * @access protected
  1160. */
  1161. protected function _stripAlias(&$args = array(), $alias = 'Model', $recurse = true, $check = 'key') {
  1162. if (!is_array($args)) {
  1163. return;
  1164. }
  1165. $checkKey = ($check === 'key' || $check === 'both');
  1166. $checkValue = ($check === 'value' || $check === 'both');
  1167. foreach($args as $key => &$val) {
  1168. if ($checkKey) {
  1169. if (strpos($key, $alias . '.') === 0) {
  1170. unset($args[$key]);
  1171. $key = substr($key, strlen($alias) + 1);
  1172. $args[$key] = $val;
  1173. }
  1174. }
  1175. if ($checkValue) {
  1176. if (is_string($val) && strpos($val, $alias . '.') === 0) {
  1177. $val = substr($val, strlen($alias) + 1);
  1178. }
  1179. }
  1180. if ($recurse && is_array($val)) {
  1181. $this->_stripAlias($val, $alias, true, $check);
  1182. }
  1183. }
  1184. }
  1185. }
  1186. /**
  1187. * MongoDbDateFormatter method
  1188. *
  1189. * This function cannot be in the class because of the way model save is written
  1190. *
  1191. * @param mixed $date null
  1192. * @return void
  1193. * @access public
  1194. */
  1195. function MongoDbDateFormatter($date = null) {
  1196. if ($date) {
  1197. return new MongoDate($date);
  1198. }
  1199. return new MongoDate();
  1200. }