MongoDb.php 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844
  1. <?php
  2. /**
  3. * Lithium: the most rad php framework
  4. *
  5. * @copyright Copyright 2013, Union of RAD (http://union-of-rad.org)
  6. * @license http://opensource.org/licenses/bsd-license.php The BSD License
  7. */
  8. namespace lithium\data\source;
  9. use MongoCode;
  10. use MongoRegex;
  11. use lithium\util\Inflector;
  12. use lithium\core\NetworkException;
  13. use Exception;
  14. /**
  15. * A data source adapter which allows you to connect to the MongoDB database engine. MongoDB is an
  16. * Open Source distributed document database which bridges the gap between key/value stores and
  17. * relational databases. To learn more about MongoDB, see here:
  18. * [http://www.mongodb.org/](http://www.mongodb.org/).
  19. *
  20. * Rather than operating on records and record sets, queries against MongoDB will return nested sets
  21. * of `Document` objects. A `Document`'s fields can contain both simple and complex data types
  22. * (i.e. arrays) including other `Document` objects.
  23. *
  24. * After installing MongoDB, you can connect to it as follows:
  25. * {{{
  26. * // config/bootstrap/connections.php:
  27. * Connections::add('default', array('type' => 'MongoDb', 'database' => 'myDb'));
  28. * }}}
  29. *
  30. * By default, it will attempt to connect to a Mongo instance running on `localhost` on port
  31. * 27017. See `__construct()` for details on the accepted configuration settings.
  32. *
  33. * @see lithium\data\entity\Document
  34. * @see lithium\data\Connections::add()
  35. * @see lithium\data\source\MongoDb::__construct()
  36. */
  37. class MongoDb extends \lithium\data\Source {
  38. /**
  39. * The Mongo class instance.
  40. *
  41. * @var object
  42. */
  43. public $server = null;
  44. /**
  45. * The MongoDB object instance.
  46. *
  47. * @var object
  48. */
  49. public $connection = null;
  50. /**
  51. * Classes used by this class.
  52. *
  53. * @var array
  54. */
  55. protected $_classes = array(
  56. 'entity' => 'lithium\data\entity\Document',
  57. 'set' => 'lithium\data\collection\DocumentSet',
  58. 'result' => 'lithium\data\source\mongo_db\Result',
  59. 'schema' => 'lithium\data\source\mongo_db\Schema',
  60. 'exporter' => 'lithium\data\source\mongo_db\Exporter',
  61. 'relationship' => 'lithium\data\model\Relationship',
  62. 'server' => 'Mongo'
  63. );
  64. /**
  65. * Map of typical SQL-like operators to their MongoDB equivalents.
  66. *
  67. * @var array Keys are SQL-like operators, value is the MongoDB equivalent.
  68. */
  69. protected $_operators = array(
  70. '<' => '$lt',
  71. '>' => '$gt',
  72. '<=' => '$lte',
  73. '>=' => '$gte',
  74. '!=' => array('single' => '$ne', 'multiple' => '$nin'),
  75. '<>' => array('single' => '$ne', 'multiple' => '$nin'),
  76. 'or' => '$or',
  77. '||' => '$or',
  78. 'not' => '$not',
  79. '!' => '$not',
  80. 'and' => '$and',
  81. '&&' => '$and',
  82. 'nor' => 'nor'
  83. );
  84. /**
  85. * List of comparison operators to use when performing boolean logic in a query.
  86. *
  87. * @var array
  88. */
  89. protected $_boolean = array('&&', '||', 'and', '$and', 'or', '$or', 'nor', '$nor');
  90. /**
  91. * A closure or anonymous function which receives an instance of this class, a collection name
  92. * and associated meta information, and returns an array defining the schema for an associated
  93. * model, where the keys are field names, and the values are arrays defining the type
  94. * information for each field. At a minimum, type arrays must contain a `'type'` key. For more
  95. * information on schema definitions, and an example schema callback implementation, see the
  96. * `$_schema` property of the `Model` class.
  97. *
  98. * @see lithium\data\Model::$_schema
  99. * @var Closure
  100. */
  101. protected $_schema = null;
  102. /**
  103. * List of configuration keys which will be automatically assigned to their corresponding
  104. * protected class properties.
  105. *
  106. * @var array
  107. */
  108. protected $_autoConfig = array('schema', 'classes' => 'merge');
  109. /**
  110. * Instantiates the MongoDB adapter with the default connection information.
  111. *
  112. * @see lithium\data\Connections::add()
  113. * @see lithium\data\source\MongoDb::$_schema
  114. * @link http://php.net/manual/en/mongo.construct.php PHP Manual: Mongo::__construct()
  115. * @param array $config All information required to connect to the database, including:
  116. * - `'database'` _string_: The name of the database to connect to. Defaults to `null`.
  117. * - `'host'` _string_: The IP or machine name where Mongo is running, followed by a
  118. * colon, and the port number. Defaults to `'localhost:27017'`.
  119. * - `'persistent'` _mixed_: Determines a persistent connection to attach to. See the
  120. * `$options` parameter of
  121. * [`Mongo::__construct()`](http://www.php.net/manual/en/mongo.construct.php) for
  122. * more information. Defaults to `false`, meaning no persistent connection is made.
  123. * - `'timeout'` _integer_: The number of milliseconds a connection attempt will wait
  124. * before timing out and throwing an exception. Defaults to `100`.
  125. * - `'schema'` _closure_: A closure or anonymous function which returns the schema
  126. * information for a model class. See the `$_schema` property for more information.
  127. * - `'gridPrefix'` _string_: The default prefix for MongoDB's `chunks` and `files`
  128. * collections. Defaults to `'fs'`.
  129. * - `'replicaSet'` _string_: See the documentation for `Mongo::__construct()`. Defaults
  130. * to `false`.
  131. * - `'readPreference'` _mixed_: May either be a single value such as Mongo::RP_NEAREST,
  132. * or an array containing a read preference and a tag set such as:
  133. * array(Mongo::RP_SECONDARY_PREFERRED, array('dc' => 'east) See the documentation for
  134. * `Mongo::setReadPreference()`. Defaults to null.
  135. *
  136. * Typically, these parameters are set in `Connections::add()`, when adding the adapter to the
  137. * list of active connections.
  138. */
  139. public function __construct(array $config = array()) {
  140. $host = 'localhost:27017';
  141. $server = $this->_classes['server'];
  142. if (class_exists($server, false)) {
  143. $host = $server::DEFAULT_HOST . ':' . $server::DEFAULT_PORT;
  144. }
  145. $defaults = compact('host') + array(
  146. 'persistent' => false,
  147. 'login' => null,
  148. 'password' => null,
  149. 'database' => null,
  150. 'timeout' => 100,
  151. 'replicaSet' => false,
  152. 'schema' => null,
  153. 'gridPrefix' => 'fs',
  154. 'safe' => false,
  155. 'readPreference' => null
  156. );
  157. parent::__construct($config + $defaults);
  158. }
  159. protected function _init() {
  160. parent::_init();
  161. $this->_operators += array('like' => function($key, $value) {
  162. return new MongoRegex($value);
  163. });
  164. }
  165. /**
  166. * Ensures that the server connection is closed and resources are freed when the adapter
  167. * instance is destroyed.
  168. *
  169. * @return void
  170. */
  171. public function __destruct() {
  172. if ($this->_isConnected) {
  173. $this->disconnect();
  174. }
  175. }
  176. /**
  177. * With no parameter, checks to see if the `mongo` extension is installed. With a parameter,
  178. * queries for a specific supported feature.
  179. *
  180. * @param string $feature Test for support for a specific feature, i.e. `"transactions"` or
  181. * `"arrays"`.
  182. * @return boolean Returns `true` if the particular feature (or if MongoDB) support is enabled,
  183. * otherwise `false`.
  184. */
  185. public static function enabled($feature = null) {
  186. if (!$feature) {
  187. return extension_loaded('mongo');
  188. }
  189. $features = array(
  190. 'arrays' => true,
  191. 'transactions' => false,
  192. 'booleans' => true,
  193. 'relationships' => true
  194. );
  195. return isset($features[$feature]) ? $features[$feature] : null;
  196. }
  197. /**
  198. * Configures a model class by overriding the default dependencies for `'set'` and
  199. * `'entity'` , and sets the primary key to `'_id'`, in keeping with Mongo's conventions.
  200. *
  201. * @see lithium\data\Model::$_meta
  202. * @see lithium\data\Model::$_classes
  203. * @param string $class The fully-namespaced model class name to be configured.
  204. * @return Returns an array containing keys `'classes'` and `'meta'`, which will be merged with
  205. * their respective properties in `Model`.
  206. */
  207. public function configureClass($class) {
  208. return array('schema' => array(), 'meta' => array('key' => '_id', 'locked' => false));
  209. }
  210. /**
  211. * Connects to the Mongo server. Matches up parameters from the constructor to create a Mongo
  212. * database connection.
  213. *
  214. * @see lithium\data\source\MongoDb::__construct()
  215. * @link http://php.net/manual/en/mongo.construct.php PHP Manual: Mongo::__construct()
  216. * @return boolean Returns `true` the connection attempt was successful, otherwise `false`.
  217. */
  218. public function connect() {
  219. if ($this->server && $this->server->connected && $this->connection) {
  220. return $this->_isConnected = true;
  221. }
  222. $cfg = $this->_config;
  223. $this->_isConnected = false;
  224. $host = is_array($cfg['host']) ? join(',', $cfg['host']) : $cfg['host'];
  225. $login = $cfg['login'] ? "{$cfg['login']}:{$cfg['password']}@" : '';
  226. $connection = "mongodb://{$login}{$host}" . ($login ? "/{$cfg['database']}" : '');
  227. $options = array(
  228. 'connect' => true,
  229. 'timeout' => $cfg['timeout'],
  230. 'replicaSet' => $cfg['replicaSet']
  231. );
  232. try {
  233. if ($persist = $cfg['persistent']) {
  234. $options['persist'] = $persist === true ? 'default' : $persist;
  235. }
  236. $server = $this->_classes['server'];
  237. $this->server = new $server($connection, $options);
  238. if ($this->connection = $this->server->{$cfg['database']}) {
  239. $this->_isConnected = true;
  240. }
  241. if ($prefs = $cfg['readPreference']) {
  242. $prefs = !is_array($prefs) ? array($prefs, array()) : $prefs;
  243. $this->server->setReadPreference($prefs[0], $prefs[1]);
  244. }
  245. } catch (Exception $e) {
  246. throw new NetworkException("Could not connect to the database.", 503, $e);
  247. }
  248. return $this->_isConnected;
  249. }
  250. /**
  251. * Disconnect from the Mongo server.
  252. *
  253. * Don't call the Mongo->close() method. The driver documentation states this should not
  254. * be necessary since it auto disconnects when out of scope.
  255. * With version 1.2.7, when using replica sets, close() can cause a segmentation fault.
  256. *
  257. * @return boolean True
  258. */
  259. public function disconnect() {
  260. if ($this->server && $this->server->connected) {
  261. $this->_isConnected = false;
  262. unset($this->connection, $this->server);
  263. }
  264. return true;
  265. }
  266. /**
  267. * Returns the list of collections in the currently-connected database.
  268. *
  269. * @param string $class The fully-name-spaced class name of the model object making the request.
  270. * @return array Returns an array of objects to which models can connect.
  271. */
  272. public function sources($class = null) {
  273. $this->_checkConnection();
  274. $conn = $this->connection;
  275. return array_map(function($col) { return $col->getName(); }, $conn->listCollections());
  276. }
  277. /**
  278. * Gets the column 'schema' for a given MongoDB collection. Only returns a schema if the
  279. * `'schema'` configuration flag has been set in the constructor.
  280. *
  281. * @see lithium\data\source\MongoDb::$_schema
  282. * @param mixed $collection Specifies a collection name for which the schema should be queried.
  283. * @param mixed $fields Any schema data pre-defined by the model.
  284. * @param array $meta Any meta information pre-defined in the model.
  285. * @return array Returns an associative array describing the given collection's schema.
  286. */
  287. public function describe($collection, $fields = array(), array $meta = array()) {
  288. if (!$fields && ($func = $this->_schema)) {
  289. $fields = $func($this, $collection, $meta);
  290. }
  291. return $this->_instance('schema', compact('fields'));
  292. }
  293. /**
  294. * Quotes identifiers.
  295. *
  296. * MongoDb does not need identifiers quoted, so this method simply returns the identifier.
  297. *
  298. * @param string $name The identifier to quote.
  299. * @return string The quoted identifier.
  300. */
  301. public function name($name) {
  302. return $name;
  303. }
  304. /**
  305. * A method dispatcher that allows direct calls to native methods in PHP's `Mongo` object. Read
  306. * more here: http://php.net/manual/class.mongo.php
  307. *
  308. * For example (assuming this instance is stored in `Connections` as `'mongo'`):
  309. * {{{// Manually repairs a MongoDB instance
  310. * Connections::get('mongo')->repairDB($db); // returns null
  311. * }}}
  312. *
  313. * @param string $method The name of native method to call. See the link above for available
  314. * class methods.
  315. * @param array $params A list of parameters to be passed to the native method.
  316. * @return mixed The return value of the native method specified in `$method`.
  317. */
  318. public function __call($method, $params) {
  319. if ((!$this->server) && !$this->connect()) {
  320. return null;
  321. }
  322. return call_user_func_array(array(&$this->server, $method), $params);
  323. }
  324. /**
  325. * Custom check to determine if our given magic methods can be responded to.
  326. *
  327. * @param string $method Method name.
  328. * @param bool $internal Interal call or not.
  329. * @return bool
  330. */
  331. public function respondsTo($method, $internal = false) {
  332. $childRespondsTo = is_object($this->server) && is_callable(array($this->server, $method));
  333. return parent::respondsTo($method, $internal) || $childRespondsTo;
  334. }
  335. /**
  336. * Normally used in cases where the query is a raw string (as opposed to a `Query` object),
  337. * to database must determine the correct column names from the result resource. Not
  338. * applicable to this data source.
  339. *
  340. * @internal param mixed $query
  341. * @internal param \lithium\data\source\resource $resource
  342. * @internal param object $context
  343. * @return array
  344. */
  345. public function schema($query, $resource = null, $context = null) {
  346. return array();
  347. }
  348. /**
  349. * Create new document
  350. *
  351. * @param string $query
  352. * @param array $options
  353. * @return boolean
  354. * @filter
  355. */
  356. public function create($query, array $options = array()) {
  357. $_config = $this->_config;
  358. $defaults = array('safe' => $_config['safe'], 'fsync' => false);
  359. $options += $defaults;
  360. $this->_checkConnection();
  361. $params = compact('query', 'options');
  362. $_exp = $this->_classes['exporter'];
  363. return $this->_filter(__METHOD__, $params, function($self, $params) use ($_config, $_exp) {
  364. $query = $params['query'];
  365. $options = $params['options'];
  366. $args = $query->export($self, array('keys' => array('source', 'data')));
  367. $data = $_exp::get('create', $args['data']);
  368. $source = $args['source'];
  369. if ($source === "{$_config['gridPrefix']}.files" && isset($data['create']['file'])) {
  370. $result = array('ok' => true);
  371. $data['create']['_id'] = $self->invokeMethod('_saveFile', array($data['create']));
  372. } else {
  373. $result = $self->connection->{$source}->insert($data['create'], $options);
  374. }
  375. if ($result === true || isset($result['ok']) && (boolean) $result['ok'] === true) {
  376. if ($query->entity()) {
  377. $query->entity()->sync($data['create']['_id']);
  378. }
  379. return true;
  380. }
  381. return false;
  382. });
  383. }
  384. protected function _saveFile($data) {
  385. $uploadKeys = array('name', 'type', 'tmp_name', 'error', 'size');
  386. $grid = $this->connection->getGridFS($this->_config['gridPrefix']);
  387. $file = null;
  388. $method = null;
  389. switch (true) {
  390. case (is_array($data['file']) && array_keys($data['file']) == $uploadKeys):
  391. if (!$data['file']['error'] && is_uploaded_file($data['file']['tmp_name'])) {
  392. $method = 'storeFile';
  393. $file = $data['file']['tmp_name'];
  394. $data['filename'] = $data['file']['name'];
  395. }
  396. break;
  397. case $data['file']:
  398. $method = 'storeBytes';
  399. $file = $data['file'];
  400. break;
  401. }
  402. if (!$method || !$file) {
  403. return;
  404. }
  405. if (isset($data['_id'])) {
  406. $data += (array) get_object_vars($grid->get($data['_id']));
  407. $grid->delete($data['_id']);
  408. }
  409. unset($data['file']);
  410. return $grid->{$method}($file, $data);
  411. }
  412. /**
  413. * Read from document
  414. *
  415. * @param string $query
  416. * @param array $options
  417. * @return object
  418. * @filter
  419. */
  420. public function read($query, array $options = array()) {
  421. $this->_checkConnection();
  422. $defaults = array('return' => 'resource');
  423. $options += $defaults;
  424. $params = compact('query', 'options');
  425. $_config = $this->_config;
  426. return $this->_filter(__METHOD__, $params, function($self, $params) use ($_config) {
  427. $query = $params['query'];
  428. $options = $params['options'];
  429. $args = $query->export($self);
  430. $source = $args['source'];
  431. if ($group = $args['group']) {
  432. $result = $self->invokeMethod('_group', array($group, $args, $options));
  433. $config = array('class' => 'set') + compact('query') + $result;
  434. return $self->item($query->model(), $config['data'], $config);
  435. }
  436. $collection = $self->connection->{$source};
  437. if ($source === "{$_config['gridPrefix']}.files") {
  438. $collection = $self->connection->getGridFS($_config['gridPrefix']);
  439. }
  440. $result = $collection->find($args['conditions'], $args['fields']);
  441. if ($query->calculate()) {
  442. return $result;
  443. }
  444. $resource = $result->sort($args['order'])->limit($args['limit'])->skip($args['offset']);
  445. $result = $self->invokeMethod('_instance', array('result', compact('resource')));
  446. $config = compact('result', 'query') + array('class' => 'set');
  447. return $self->item($query->model(), array(), $config);
  448. });
  449. }
  450. protected function _group($group, $args, $options) {
  451. $conditions = $args['conditions'];
  452. $group += array('$reduce' => $args['reduce'], 'initial' => $args['initial']);
  453. $command = array('group' => $group + array('ns' => $args['source'], 'cond' => $conditions));
  454. $stats = $this->connection->command($command);
  455. $data = isset($stats['retval']) ? $stats['retval'] : null;
  456. unset($stats['retval']);
  457. return compact('data', 'stats');
  458. }
  459. /**
  460. * Update document
  461. *
  462. * @param string $query
  463. * @param array $options
  464. * @return boolean
  465. * @filter
  466. */
  467. public function update($query, array $options = array()) {
  468. $_config = $this->_config;
  469. $defaults = array(
  470. 'upsert' => false,
  471. 'multiple' => true,
  472. 'safe' => $_config['safe'],
  473. 'fsync' => false
  474. );
  475. $options += $defaults;
  476. $this->_checkConnection();
  477. $params = compact('query', 'options');
  478. $_exp = $this->_classes['exporter'];
  479. return $this->_filter(__METHOD__, $params, function($self, $params) use ($_config, $_exp) {
  480. $options = $params['options'];
  481. $query = $params['query'];
  482. $args = $query->export($self, array('keys' => array('conditions', 'source', 'data')));
  483. $source = $args['source'];
  484. $data = $args['data'];
  485. if ($query->entity()) {
  486. $data = $_exp::get('update', $data);
  487. }
  488. if ($source === "{$_config['gridPrefix']}.files" && isset($data['update']['file'])) {
  489. $args['data']['_id'] = $self->invokeMethod('_saveFile', array($data['update']));
  490. }
  491. $update = $query->entity() ? $_exp::toCommand($data) : $data;
  492. if ($options['multiple'] && !preg_grep('/^\$/', array_keys($update))) {
  493. $update = array('$set' => $update);
  494. }
  495. if ($self->connection->{$source}->update($args['conditions'], $update, $options)) {
  496. $query->entity() ? $query->entity()->sync() : null;
  497. return true;
  498. }
  499. return false;
  500. });
  501. }
  502. /**
  503. * Delete document
  504. *
  505. * @param string $query
  506. * @param array $options
  507. * @return boolean
  508. * @filter
  509. */
  510. public function delete($query, array $options = array()) {
  511. $this->_checkConnection();
  512. $_config = $this->_config;
  513. $defaults = array('justOne' => false, 'safe' => $_config['safe'], 'fsync' => false);
  514. $options = array_intersect_key($options + $defaults, $defaults);
  515. $params = compact('query', 'options');
  516. return $this->_filter(__METHOD__, $params, function($self, $params) use ($_config) {
  517. $query = $params['query'];
  518. $options = $params['options'];
  519. $args = $query->export($self, array('keys' => array('source', 'conditions')));
  520. $source = $args['source'];
  521. $conditions = $args['conditions'];
  522. if ($source === "{$_config['gridPrefix']}.files") {
  523. $result = $self->invokeMethod('_deleteFile', array($conditions));
  524. } else {
  525. $result = $self->connection->{$args['source']}->remove($conditions, $options);
  526. }
  527. if ($result && $query->entity()) {
  528. $query->entity()->sync(null, array(), array('dematerialize' => true));
  529. }
  530. return $result;
  531. });
  532. }
  533. protected function _deleteFile($conditions, $options = array()) {
  534. $defaults = array('safe' => true);
  535. $options += $defaults;
  536. return $this->connection
  537. ->getGridFS($this->_config['gridPrefix'])
  538. ->remove($conditions, $options);
  539. }
  540. /**
  541. * Executes calculation-related queries, such as those required for `count`.
  542. *
  543. * @param string $type Only accepts `count`.
  544. * @param mixed $query The query to be executed.
  545. * @param array $options Optional arguments for the `read()` query that will be executed
  546. * to obtain the calculation result.
  547. * @return integer Result of the calculation.
  548. */
  549. public function calculation($type, $query, array $options = array()) {
  550. $query->calculate($type);
  551. switch ($type) {
  552. case 'count':
  553. return $this->read($query, $options)->count();
  554. }
  555. }
  556. /**
  557. * Document relationships.
  558. *
  559. * @param string $class
  560. * @param string $type Relationship type, e.g. `belongsTo`.
  561. * @param string $name
  562. * @param array $config
  563. * @return array
  564. */
  565. public function relationship($class, $type, $name, array $config = array()) {
  566. $key = Inflector::camelize($type === 'belongsTo' ? $class::meta('name') : $name, false);
  567. $config += compact('name', 'type', 'key');
  568. $config['from'] = $class;
  569. $relationship = $this->_classes['relationship'];
  570. $defaultLinks = array(
  571. 'hasOne' => $relationship::LINK_EMBEDDED,
  572. 'hasMany' => $relationship::LINK_EMBEDDED,
  573. 'belongsTo' => $relationship::LINK_CONTAINED
  574. );
  575. $config += array('link' => $defaultLinks[$type]);
  576. return new $relationship($config);
  577. }
  578. /**
  579. * Formats `group` clauses for MongoDB.
  580. *
  581. * @param string|array $group The group clause.
  582. * @param object $context
  583. * @return array Formatted `group` clause.
  584. */
  585. public function group($group, $context) {
  586. if (!$group) {
  587. return;
  588. }
  589. if (is_string($group) && strpos($group, 'function') === 0) {
  590. return array('$keyf' => new MongoCode($group));
  591. }
  592. $group = (array) $group;
  593. foreach ($group as $i => $field) {
  594. if (is_int($i)) {
  595. $group[$field] = true;
  596. unset($group[$i]);
  597. }
  598. }
  599. return array('key' => $group);
  600. }
  601. /**
  602. * Maps incoming conditions with their corresponding MongoDB-native operators.
  603. *
  604. * @param array $conditions Array of conditions
  605. * @param object $context Context with which this method was called; currently
  606. * inspects the return value of `$context->type()`.
  607. * @return array Transformed conditions
  608. */
  609. public function conditions($conditions, $context) {
  610. if (!$conditions) {
  611. return array();
  612. }
  613. if ($code = $this->_isMongoCode($conditions)) {
  614. return $code;
  615. }
  616. $schema = null;
  617. $model = null;
  618. if ($context) {
  619. $schema = $context->schema();
  620. $model = $context->model();
  621. }
  622. return $this->_conditions($conditions, $model, $schema, $context);
  623. }
  624. /**
  625. * Protected helper method used to format conditions.
  626. *
  627. * @param array $conditions The conditions array to be processed.
  628. * @param string $model The name of the model class used in the query.
  629. * @param object $schema The object containing the schema definition.
  630. * @param object $context The `Query` object.
  631. * @return array Processed query conditions.
  632. */
  633. protected function _conditions(array $conditions, $model, $schema, $context) {
  634. $ops = $this->_operators;
  635. $castOpts = array('first' => true, 'database' => $this, 'wrap' => false);
  636. $cast = function($key, $value) use (&$schema, &$castOpts) {
  637. return $schema ? $schema->cast(null, $key, $value, $castOpts) : $value;
  638. };
  639. foreach ($conditions as $key => $value) {
  640. if (in_array($key, $this->_boolean)) {
  641. $operator = isset($ops[$key]) ? $ops[$key] : $key;
  642. foreach ($value as $i => $compare) {
  643. $value[$i] = $this->_conditions($compare, $model, $schema, $context);
  644. }
  645. unset($conditions[$key]);
  646. $conditions[$operator] = $value;
  647. continue;
  648. }
  649. /**
  650. * @todo Catch Document/Array objects used in conditions and extract their values.
  651. */
  652. if (is_object($value)) {
  653. continue;
  654. }
  655. if (!is_array($value)) {
  656. $conditions[$key] = $cast($key, $value);
  657. continue;
  658. }
  659. $current = key($value);
  660. if (!isset($ops[$current]) && $current[0] !== '$') {
  661. $conditions[$key] = array('$in' => $cast($key, $value));
  662. continue;
  663. }
  664. $conditions[$key] = $this->_operators($key, $value, $schema);
  665. }
  666. return $conditions;
  667. }
  668. protected function _isMongoCode($conditions) {
  669. if (is_string($conditions)) {
  670. $conditions = new MongoCode($conditions);
  671. }
  672. if ($conditions instanceof MongoCode) {
  673. return array('$where' => $conditions);
  674. }
  675. }
  676. protected function _operators($field, $operators, $schema) {
  677. $castOpts = compact('schema');
  678. $castOpts += array('first' => true, 'database' => $this, 'wrap' => false);
  679. $cast = function($key, $value) use (&$schema, &$castOpts) {
  680. return $schema ? $schema->cast(null, $key, $value, $castOpts) : $value;
  681. };
  682. foreach ($operators as $key => $value) {
  683. if (!isset($this->_operators[$key])) {
  684. $operators[$key] = $cast($field, $value);
  685. continue;
  686. }
  687. $operator = $this->_operators[$key];
  688. if (is_array($operator)) {
  689. $operator = $operator[is_array($value) ? 'multiple' : 'single'];
  690. }
  691. if (is_callable($operator)) {
  692. return $operator($key, $value, $schema);
  693. }
  694. unset($operators[$key]);
  695. $operators[$operator] = $cast($field, $value);
  696. }
  697. return $operators;
  698. }
  699. /**
  700. * Return formatted identifiers for fields.
  701. *
  702. * MongoDB does nt require field identifer escaping; as a result, this method is not
  703. * implemented.
  704. *
  705. * @param array $fields Fields to be parsed
  706. * @param object $context
  707. * @return array Parsed fields array
  708. */
  709. public function fields($fields, $context) {
  710. return $fields ?: array();
  711. }
  712. /**
  713. * Return formatted clause for limit.
  714. *
  715. * MongoDB doesn't require limit identifer formatting; as a result, this method is not
  716. * implemented.
  717. *
  718. * @param mixed $limit The `limit` clause to be formatted.
  719. * @param object $context The `Query` object instance.
  720. * @return mixed Formatted `limit` clause.
  721. */
  722. public function limit($limit, $context) {
  723. return $limit ?: 0;
  724. }
  725. /**
  726. * Return formatted clause for order.
  727. *
  728. * @param mixed $order The `order` clause to be formatted
  729. * @param object $context
  730. * @return mixed Formatted `order` clause.
  731. */
  732. public function order($order, $context) {
  733. if (!$order) {
  734. return array();
  735. }
  736. if (is_string($order)) {
  737. return array($order => 1);
  738. }
  739. if (!is_array($order)) {
  740. return array();
  741. }
  742. foreach ($order as $key => $value) {
  743. if (!is_string($key)) {
  744. unset($order[$key]);
  745. $order[$value] = 1;
  746. continue;
  747. }
  748. if (is_string($value)) {
  749. $order[$key] = strtolower($value) === 'asc' ? 1 : -1;
  750. }
  751. }
  752. return $order;
  753. }
  754. protected function _checkConnection() {
  755. if (!$this->_isConnected && !$this->connect()) {
  756. throw new NetworkException("Could not connect to the database.");
  757. }
  758. }
  759. }
  760. ?>