PageRenderTime 57ms CodeModel.GetById 18ms RepoModel.GetById 0ms app.codeStats 0ms

/data/source/MongoDb.php

http://github.com/UnionOfRAD/lithium
PHP | 1080 lines | 593 code | 102 blank | 385 comment | 87 complexity | 91ff82bb1cd29f3c11adda542c10f79c MD5 | raw file
  1. <?php
  2. /**
  3. * li₃: the most RAD framework for PHP (http://li3.me)
  4. *
  5. * Copyright 2009, Union of RAD. All rights reserved. This source
  6. * code is distributed under the terms of the BSD 3-Clause License.
  7. * The full license text can be found in the LICENSE.txt file.
  8. */
  9. namespace lithium\data\source;
  10. use Exception;
  11. use MongoCode;
  12. use MongoRegex;
  13. use lithium\aop\Filters;
  14. use lithium\core\Libraries;
  15. use lithium\core\NetworkException;
  16. use lithium\net\HostString;
  17. use lithium\util\Inflector;
  18. /**
  19. * A data source adapter which allows you to connect to the MongoDB database engine. MongoDB is an
  20. * Open Source distributed document database which bridges the gap between key/value stores and
  21. * relational databases.
  22. *
  23. * Rather than operating on records and record sets, queries against MongoDB will return nested sets
  24. * of `Document` objects. A `Document`'s fields can contain both simple and complex data types
  25. * (i.e. arrays) including other `Document` objects.
  26. *
  27. * After installing MongoDB, you can connect to it as follows:
  28. * ```
  29. * // config/bootstrap/connections.php:
  30. * Connections::add('default', ['type' => 'MongoDb', 'database' => 'myDb']);
  31. * ```
  32. *
  33. * By default, it will attempt to connect to a Mongo instance running on `localhost` on port
  34. * 27017. See `__construct()` for details on the accepted configuration settings.
  35. *
  36. * This adapter is officially supported on PHP 5, where it simply needs the `mongo`
  37. * extension. Usage on top of PHP 7 is unofficially supported by using the new `mongodb`
  38. * extension in conjunction with a compatibility layer (i.e. `mongo-php-adapter`).
  39. *
  40. * @see lithium\data\entity\Document
  41. * @see lithium\data\Connections::add()
  42. * @see lithium\data\source\MongoDb::__construct()
  43. * @link https://pecl.php.net/package/mongo
  44. * @link http://www.mongodb.org/
  45. * @link https://github.com/alcaeus/mongo-php-adapter
  46. */
  47. class MongoDb extends \lithium\data\Source {
  48. /**
  49. * The default host used to connect to the server.
  50. */
  51. const DEFAULT_HOST = 'localhost';
  52. /**
  53. * The default port used to connect to the server.
  54. */
  55. const DEFAULT_PORT = 27017;
  56. /**
  57. * The Mongo class instance.
  58. *
  59. * @var object
  60. */
  61. public $server = null;
  62. /**
  63. * The MongoDB object instance.
  64. *
  65. * @var object
  66. */
  67. public $connection = null;
  68. /**
  69. * Classes used by this class.
  70. *
  71. * @var array
  72. */
  73. protected $_classes = [
  74. 'entity' => 'lithium\data\entity\Document',
  75. 'set' => 'lithium\data\collection\DocumentSet',
  76. 'result' => 'lithium\data\source\mongo_db\Result',
  77. 'schema' => 'lithium\data\source\mongo_db\Schema',
  78. 'exporter' => 'lithium\data\source\mongo_db\Exporter',
  79. 'relationship' => 'lithium\data\model\Relationship',
  80. 'server' => 'MongoClient'
  81. ];
  82. /**
  83. * Map of typical SQL-like operators to their MongoDB equivalents.
  84. *
  85. * @var array Keys are SQL-like operators, value is the MongoDB equivalent.
  86. */
  87. protected $_operators = [
  88. '<' => '$lt',
  89. '>' => '$gt',
  90. '<=' => '$lte',
  91. '>=' => '$gte',
  92. '!=' => ['single' => '$ne', 'multiple' => '$nin'],
  93. '<>' => ['single' => '$ne', 'multiple' => '$nin'],
  94. 'or' => '$or',
  95. '||' => '$or',
  96. 'not' => '$not',
  97. '!' => '$not',
  98. 'and' => '$and',
  99. '&&' => '$and',
  100. 'nor' => '$nor'
  101. ];
  102. /**
  103. * List of comparison operators to use when performing boolean logic in a query.
  104. *
  105. * @var array
  106. */
  107. protected $_boolean = ['&&', '||', 'and', '$and', 'or', '$or', 'nor', '$nor'];
  108. /**
  109. * A closure or anonymous function which receives an instance of this class, a
  110. * collection name and associated meta information, and returns an array defining the
  111. * schema for an associated model, where the keys are field names, and the values are
  112. * arrays defining the type information for each field. At a minimum, type arrays
  113. * must contain a `'type'` key. For more information on schema definitions see the
  114. * `$_schema` property of the `Model` class.
  115. *
  116. * This example shows how to implement a schema callback in your database connection
  117. * configuration that fetches and returns the schema data. It defines an optional
  118. * MongoDB convention in which the schema for each individual collection is stored
  119. * in a `schemas` collection, where each document contains the name of a collection,
  120. * along with a `'data'` key, which contains the schema for that collection.
  121. *
  122. * ```
  123. * Connections::add('default', [
  124. * 'type' => 'MongoDb',
  125. * 'host' => 'localhost',
  126. * 'database' => 'app',
  127. * 'schema' => function($db, $collection, $meta) {
  128. * $result = $db->connection->schemas->findOne(compact('collection'));
  129. * return $result ? $result['data'] : [];
  130. * }
  131. * ]);
  132. * ```
  133. *
  134. * A complete schema defintion looks like:
  135. * ```
  136. * [
  137. * '_id' => ['type' => 'id'],
  138. * 'name' => ['type' => 'string', 'default' => 'Moe', 'null' => false],
  139. * 'sign' => ['type' => 'string', 'default' => 'bar', 'null' => false],
  140. * 'age' => ['type' => 'integer', 'default' => 0, 'null' => false]
  141. * ];
  142. * ```
  143. *
  144. * The tyes in the schema map to database native type like this:
  145. * ```
  146. * id => MongoId
  147. * date => MongoDate
  148. * regex => MongoRegex
  149. * integer => integer
  150. * float => float
  151. * boolean => boolean
  152. * code => MongoCode
  153. * binary => MongoBinData
  154. * ```
  155. *
  156. * @see lithium\data\Model::$_schema
  157. * @var Closure|null
  158. */
  159. protected $_schema = null;
  160. /**
  161. * List of configuration keys which will be automatically assigned to their corresponding
  162. * protected class properties.
  163. *
  164. * @var array
  165. */
  166. protected $_autoConfig = ['schema', 'classes' => 'merge'];
  167. /**
  168. * With no parameter, checks to see if adapter's dependencies are installed. With a
  169. * parameter, queries for a specific supported feature.
  170. *
  171. * A compatibility layer cannot be detected via `extension_loaded()`, thus we check
  172. * for the existence of one of the legacy classes to determine if this adapter can be
  173. * enabled at all.
  174. *
  175. * @param string $feature Test for support for a specific feature, i.e. `"transactions"` or
  176. * `"arrays"`.
  177. * @return boolean Returns `true` if the particular feature (or if MongoDB) support is enabled,
  178. * otherwise `false`.
  179. */
  180. public static function enabled($feature = null) {
  181. if (!$feature) {
  182. return class_exists('MongoClient');
  183. }
  184. $features = [
  185. 'arrays' => true,
  186. 'transactions' => false,
  187. 'booleans' => true,
  188. 'relationships' => true,
  189. 'schema' => false,
  190. 'sources' => true
  191. ];
  192. return isset($features[$feature]) ? $features[$feature] : null;
  193. }
  194. /**
  195. * Constructor.
  196. *
  197. * @see lithium\data\Connections::add()
  198. * @see lithium\data\source\MongoDb::$_schema
  199. * @link http://php.net/mongo.construct.php PHP Manual: Mongo::__construct()
  200. * @param array $config Configuration options required to connect to the database, including:
  201. * - `'host'` _string|array_: A string in the form of `'<host>'`, `'<host>:<port>'` or
  202. * `':<port>'` indicating the host and/or port to connect to. When one or both are
  203. * not provided uses general server defaults (if possible retrieved from the client
  204. * implementation).
  205. * Use the array format for multiple hosts:
  206. * `array('167.221.1.5:11222', '167.221.1.6')`
  207. * - `'database'` _string_: The name of the database to connect to. Defaults to `null`.
  208. * - `'timeout'` _integer_: The number of milliseconds a connection attempt will wait
  209. * before timing out and throwing an exception. Defaults to `100`.
  210. * - `'schema'` _\Closure_: A closure or anonymous function which returns the schema
  211. * information for a model class. See the `$_schema` property for more information.
  212. * - `'gridPrefix'` _string_: The default prefix for MongoDB's `chunks` and `files`
  213. * collections. Defaults to `'fs'`.
  214. * - `'replicaSet'` _string_: See the documentation for `Mongo::__construct()`. Defaults
  215. * to `false`.
  216. * - `'readPreference'` _mixed_: May either be a single value such as Mongo::RP_NEAREST,
  217. * or an array containing a read preference and a tag set such as:
  218. * (Mongo::RP_SECONDARY_PREFERRED, ['dc' => 'east] See the documentation for
  219. * `Mongo::setReadPreference()`. Defaults to null.
  220. * Typically, these parameters are set in `Connections::add()`, when adding the
  221. * adapter to the list of active connections.
  222. *
  223. * Disables auto-connect, which is by default enabled in `Source`. Instead before
  224. * each query execution the connection is checked and if needed (re-)established.
  225. * @return void
  226. */
  227. public function __construct(array $config = []) {
  228. if (class_exists($server = $this->_classes['server'], false)) {
  229. $defaultHost = $server::DEFAULT_HOST . ':' . $server::DEFAULT_PORT;
  230. } else {
  231. $defaultHost = static::DEFAULT_HOST . ':' . static::DEFAULT_PORT;
  232. }
  233. $defaults = [
  234. 'host' => $defaultHost,
  235. 'login' => null,
  236. 'password' => null,
  237. 'database' => null,
  238. 'timeout' => 100,
  239. 'replicaSet' => false,
  240. 'schema' => null,
  241. 'gridPrefix' => 'fs',
  242. 'w' => 1,
  243. 'wTimeoutMS' => 10000,
  244. 'readPreference' => null,
  245. 'autoConnect' => false,
  246. 'dsn' => null
  247. ];
  248. parent::__construct($config + $defaults);
  249. }
  250. /**
  251. * Initializer. Adds operator handlers which will later allow to correctly cast any
  252. * values. Constructs a DSN from configuration.
  253. *
  254. * @see lithium\data\source\MongoDb::$_operators
  255. * @see lithium\data\source\MongoDb::_operators()
  256. * @return void
  257. */
  258. protected function _init() {
  259. $hosts = [];
  260. foreach ((array) $this->_config['host'] as $host) {
  261. $host = HostString::parse($host) + [
  262. 'host' => static::DEFAULT_HOST,
  263. 'port' => static::DEFAULT_PORT
  264. ];
  265. $hosts[] = "{$host['host']}:{$host['port']}";
  266. }
  267. if ($this->_config['login']) {
  268. $this->_config['dsn'] = sprintf(
  269. 'mongodb://%s:%s@%s/%s',
  270. $this->_config['login'],
  271. $this->_config['password'],
  272. implode(',', $hosts),
  273. $this->_config['database']
  274. );
  275. } else {
  276. $this->_config['dsn'] = sprintf(
  277. 'mongodb://%s',
  278. implode(',', $hosts)
  279. );
  280. }
  281. parent::_init();
  282. $this->_operators += [
  283. 'like' => function($key, $value) {
  284. return new MongoRegex($value);
  285. },
  286. '$exists' => function($key, $value) {
  287. return ['$exists' => (boolean) $value];
  288. },
  289. '$type' => function($key, $value) {
  290. return ['$type' => (integer) $value];
  291. },
  292. '$mod' => function($key, $value) {
  293. $value = (array) $value;
  294. return ['$mod' => [current($value), next($value) ?: 0]];
  295. },
  296. '$size' => function($key, $value) {
  297. return ['$size' => (integer) $value];
  298. },
  299. '$elemMatch' => function($operator, $values, $options = []) {
  300. $options += [
  301. 'castOpts' => [],
  302. 'field' => ''
  303. ];
  304. $options['castOpts'] += ['pathKey' => $options['field']];
  305. $values = (array) $values;
  306. if (empty($options['castOpts']['schema'])) {
  307. return ['$elemMatch' => $values];
  308. }
  309. foreach ($values as $key => &$value) {
  310. $value = $options['castOpts']['schema']->cast(
  311. null, $key, $value, $options['castOpts']
  312. );
  313. }
  314. return ['$elemMatch' => $values];
  315. }
  316. ];
  317. }
  318. /**
  319. * Destructor. Ensures that the server connection is closed and resources are freed when
  320. * the adapter instance is destroyed.
  321. *
  322. * @return void
  323. */
  324. public function __destruct() {
  325. if ($this->_isConnected) {
  326. $this->disconnect();
  327. }
  328. }
  329. /**
  330. * Configures a model class by overriding the default dependencies for `'set'` and
  331. * `'entity'` , and sets the primary key to `'_id'`, in keeping with Mongo's conventions.
  332. *
  333. * @see lithium\data\Model::$_meta
  334. * @see lithium\data\Model::$_classes
  335. * @param string $class The fully-namespaced model class name to be configured.
  336. * @return Returns an array containing keys `'classes'` and `'meta'`, which will be merged with
  337. * their respective properties in `Model`.
  338. */
  339. public function configureClass($class) {
  340. return [
  341. 'classes' => $this->_classes,
  342. 'schema' => [],
  343. 'meta' => ['key' => '_id', 'locked' => false]
  344. ];
  345. }
  346. /**
  347. * Connects to the Mongo server. Matches up parameters from the constructor to create a Mongo
  348. * database connection.
  349. *
  350. * @see lithium\data\source\MongoDb::__construct()
  351. * @link http://php.net/mongo.construct.php PHP Manual: Mongo::__construct()
  352. * @return boolean Returns `true` the connection attempt was successful, otherwise `false`.
  353. */
  354. public function connect() {
  355. $server = $this->_classes['server'];
  356. if ($this->server && $this->server->getConnections() && $this->connection) {
  357. return $this->_isConnected = true;
  358. }
  359. $this->_isConnected = false;
  360. $options = [
  361. 'connect' => true,
  362. 'connectTimeoutMS' => $this->_config['timeout'],
  363. 'replicaSet' => $this->_config['replicaSet'],
  364. ];
  365. try {
  366. $this->server = new $server($this->_config['dsn'], $options);
  367. if ($prefs = $this->_config['readPreference']) {
  368. $prefs = !is_array($prefs) ? [$prefs, []] : $prefs;
  369. $this->server->setReadPreference($prefs[0], $prefs[1]);
  370. }
  371. if ($this->connection = $this->server->{$this->_config['database']}) {
  372. $this->_isConnected = true;
  373. }
  374. } catch (Exception $e) {
  375. throw new NetworkException("Could not connect to the database.", 503, $e);
  376. }
  377. return $this->_isConnected;
  378. }
  379. /**
  380. * Disconnect from the Mongo server.
  381. *
  382. * Don't call the Mongo->close() method. The driver documentation states this should not
  383. * be necessary since it auto disconnects when out of scope.
  384. * With version 1.2.7, when using replica sets, close() can cause a segmentation fault.
  385. *
  386. * @return boolean True
  387. */
  388. public function disconnect() {
  389. if ($this->server && $this->server->getConnections()) {
  390. $this->_isConnected = false;
  391. unset($this->connection, $this->server);
  392. }
  393. return true;
  394. }
  395. /**
  396. * Returns the list of collections in the currently-connected database.
  397. *
  398. * @param string $class The fully-name-spaced class name of the model object making the request.
  399. * @return array Returns an array of objects to which models can connect.
  400. */
  401. public function sources($class = null) {
  402. $this->_checkConnection();
  403. $conn = $this->connection;
  404. return array_map(function($col) { return $col->getName(); }, $conn->listCollections());
  405. }
  406. /**
  407. * Gets the column 'schema' for a given MongoDB collection. Only returns a schema if the
  408. * `'schema'` configuration flag has been set in the constructor.
  409. *
  410. * @see lithium\data\source\MongoDb::$_schema
  411. * @param mixed $collection Specifies a collection name for which the schema should be queried.
  412. * @param mixed $fields Any schema data pre-defined by the model.
  413. * @param array $meta Any meta information pre-defined in the model.
  414. * @return array Returns an associative array describing the given collection's schema.
  415. */
  416. public function describe($collection, $fields = [], array $meta = []) {
  417. if (!$fields && ($func = $this->_schema)) {
  418. $fields = $func($this, $collection, $meta);
  419. }
  420. return Libraries::instance(null, 'schema', compact('fields'), $this->_classes);
  421. }
  422. /**
  423. * Quotes identifiers.
  424. *
  425. * MongoDb does not need identifiers quoted, so this method simply returns the identifier.
  426. *
  427. * @param string $name The identifier to quote.
  428. * @return string The quoted identifier.
  429. */
  430. public function name($name) {
  431. return $name;
  432. }
  433. /**
  434. * A method dispatcher that allows direct calls to native methods in PHP's `Mongo` object. Read
  435. * more here: http://php.net/manual/class.mongo.php
  436. *
  437. * For example (assuming this instance is stored in `Connections` as `'mongo'`):
  438. * ```
  439. * // Manually repairs a MongoDB instance
  440. * Connections::get('mongo')->repairDB($db); // returns null
  441. * ```
  442. *
  443. * @param string $method The name of native method to call. See the link above for available
  444. * class methods.
  445. * @param array $params A list of parameters to be passed to the native method.
  446. * @return mixed The return value of the native method specified in `$method`.
  447. */
  448. public function __call($method, $params) {
  449. if ((!$this->server) && !$this->connect()) {
  450. return null;
  451. }
  452. return call_user_func_array([&$this->server, $method], $params);
  453. }
  454. /**
  455. * Determines if a given method can be called.
  456. *
  457. * @deprecated
  458. * @param string $method Name of the method.
  459. * @param boolean $internal Provide `true` to perform check from inside the
  460. * class/object. When `false` checks also for public visibility;
  461. * defaults to `false`.
  462. * @return boolean Returns `true` if the method can be called, `false` otherwise.
  463. */
  464. public function respondsTo($method, $internal = false) {
  465. $message = '`' . __METHOD__ . '()` has been deprecated. ';
  466. $message .= 'Use `is_callable([$adapter->server, \'<method>\'])` instead.';
  467. trigger_error($message, E_USER_DEPRECATED);
  468. $childRespondsTo = is_object($this->server) && is_callable([$this->server, $method]);
  469. return parent::respondsTo($method, $internal) || $childRespondsTo;
  470. }
  471. /**
  472. * Normally used in cases where the query is a raw string (as opposed to a `Query` object),
  473. * to database must determine the correct column names from the result resource. Not
  474. * applicable to this data source.
  475. *
  476. * @internal param mixed $query
  477. * @internal param \lithium\data\source\resource $resource
  478. * @internal param object $context
  479. * @return array
  480. */
  481. public function schema($query, $resource = null, $context = null) {
  482. return [];
  483. }
  484. /**
  485. * Create new document
  486. *
  487. * @param string $query
  488. * @param array $options
  489. * @return boolean
  490. * @filter
  491. */
  492. public function create($query, array $options = []) {
  493. $this->_checkConnection();
  494. $defaults = [
  495. 'w' => $this->_config['w'],
  496. 'wTimeoutMS' => $this->_config['wTimeoutMS'],
  497. 'fsync' => false
  498. ];
  499. $options += $defaults;
  500. $params = compact('query', 'options');
  501. return Filters::run($this, __FUNCTION__, $params, function($params) {
  502. $exporter = $this->_classes['exporter'];
  503. $prefix = $this->_config['gridPrefix'];
  504. $query = $params['query'];
  505. $options = $params['options'];
  506. $args = $query->export($this, ['keys' => ['source', 'data']]);
  507. $data = $exporter::get('create', $args['data']);
  508. $source = $args['source'];
  509. if ($source === "{$prefix}.files" && isset($data['create']['file'])) {
  510. $result = ['ok' => true];
  511. $data['create']['_id'] = $this->_saveFile($data['create']);
  512. } else {
  513. $result = $this->connection->{$source}->insert($data['create'], $options);
  514. $result = $this->_ok($result);
  515. }
  516. if ($result === true || isset($result['ok']) && (boolean) $result['ok'] === true) {
  517. if ($query->entity()) {
  518. $query->entity()->sync($data['create']['_id']);
  519. }
  520. return true;
  521. }
  522. return false;
  523. });
  524. }
  525. protected function _saveFile($data) {
  526. $uploadKeys = ['name', 'type', 'tmp_name', 'error', 'size'];
  527. $grid = $this->connection->getGridFS($this->_config['gridPrefix']);
  528. $file = null;
  529. $method = null;
  530. switch (true) {
  531. case (is_array($data['file']) && array_keys($data['file']) == $uploadKeys):
  532. if (!$data['file']['error'] && is_uploaded_file($data['file']['tmp_name'])) {
  533. $method = 'storeFile';
  534. $file = $data['file']['tmp_name'];
  535. $data['filename'] = $data['file']['name'];
  536. }
  537. break;
  538. case $data['file']:
  539. $method = 'storeBytes';
  540. $file = $data['file'];
  541. break;
  542. }
  543. if (!$method || !$file) {
  544. return;
  545. }
  546. if (isset($data['_id'])) {
  547. $data += (array) get_object_vars($grid->get($data['_id']));
  548. $grid->delete($data['_id']);
  549. }
  550. unset($data['file']);
  551. return $grid->{$method}($file, $data);
  552. }
  553. /**
  554. * Read from document
  555. *
  556. * @param string $query
  557. * @param array $options
  558. * @return object
  559. * @filter
  560. */
  561. public function read($query, array $options = []) {
  562. $this->_checkConnection();
  563. $defaults = ['return' => 'resource'];
  564. $options += $defaults;
  565. $params = compact('query', 'options');
  566. return Filters::run($this, __FUNCTION__, $params, function($params) {
  567. $prefix = $this->_config['gridPrefix'];
  568. $query = $params['query'];
  569. $options = $params['options'];
  570. $args = $query->export($this);
  571. $source = $args['source'];
  572. $model = $query->model();
  573. if ($group = $args['group']) {
  574. $result = $this->_group($group, $args, $options);
  575. $config = ['class' => 'set', 'defaults' => false] + compact('query') + $result;
  576. return $model::create($config['data'], $config);
  577. }
  578. $collection = $this->connection->{$source};
  579. if ($source === "{$prefix}.files") {
  580. $collection = $this->connection->getGridFS($prefix);
  581. }
  582. $result = $collection->find($args['conditions'], $args['fields']);
  583. if ($query->calculate()) {
  584. return $result;
  585. }
  586. $resource = $result->sort($args['order'])->limit($args['limit'])->skip($args['offset']);
  587. $result = Libraries::instance(null, 'result', compact('resource'), $this->_classes);
  588. $config = compact('result', 'query') + ['class' => 'set', 'defaults' => false];
  589. $collection = $model::create([], $config);
  590. if (is_object($query) && $query->with()) {
  591. $model::embed($collection, $query->with());
  592. }
  593. return $collection;
  594. });
  595. }
  596. protected function _group($group, $args, $options) {
  597. $conditions = $args['conditions'];
  598. $group += ['$reduce' => $args['reduce'], 'initial' => $args['initial']];
  599. $command = ['group' => $group + ['ns' => $args['source'], 'cond' => $conditions]];
  600. $stats = $this->connection->command($command);
  601. $data = isset($stats['retval']) ? $stats['retval'] : null;
  602. unset($stats['retval']);
  603. return compact('data', 'stats');
  604. }
  605. /**
  606. * Update document
  607. *
  608. * @param string $query
  609. * @param array $options
  610. * @return boolean
  611. * @filter
  612. */
  613. public function update($query, array $options = []) {
  614. $this->_checkConnection();
  615. $defaults = [
  616. 'upsert' => false,
  617. 'multiple' => true,
  618. 'w' => $this->_config['w'],
  619. 'wTimeoutMS' => $this->_config['wTimeoutMS'],
  620. 'fsync' => false
  621. ];
  622. $options += $defaults;
  623. $params = compact('query', 'options');
  624. return Filters::run($this, __FUNCTION__, $params, function($params) {
  625. $exporter = $this->_classes['exporter'];
  626. $prefix = $this->_config['gridPrefix'];
  627. $options = $params['options'];
  628. $query = $params['query'];
  629. $args = $query->export($this, ['keys' => ['conditions', 'source', 'data']]);
  630. $source = $args['source'];
  631. $data = $args['data'];
  632. if ($query->entity()) {
  633. $data = $exporter::get('update', $data);
  634. }
  635. if ($source === "{$prefix}.files" && isset($data['update']['file'])) {
  636. $args['data']['_id'] = $this->_saveFile($data['update']);
  637. }
  638. $update = $query->entity() ? $exporter::toCommand($data) : $data;
  639. if (empty($update)) {
  640. return true;
  641. }
  642. if ($options['multiple'] && !preg_grep('/^\$/', array_keys($update))) {
  643. $update = ['$set' => $update];
  644. }
  645. $result = $this->connection->{$source}->update($args['conditions'], $update, $options);
  646. if ($this->_ok($result)) {
  647. $query->entity() ? $query->entity()->sync() : null;
  648. return true;
  649. }
  650. return false;
  651. });
  652. }
  653. /**
  654. * Delete document
  655. *
  656. * @param string $query
  657. * @param array $options
  658. * @return boolean
  659. * @filter
  660. */
  661. public function delete($query, array $options = []) {
  662. $this->_checkConnection();
  663. $defaults = [
  664. 'justOne' => false,
  665. 'w' => $this->_config['w'],
  666. 'wTimeoutMS' => $this->_config['wTimeoutMS'],
  667. 'fsync' => false
  668. ];
  669. $options = array_intersect_key($options + $defaults, $defaults);
  670. $params = compact('query', 'options');
  671. return Filters::run($this, __FUNCTION__, $params, function($params) {
  672. $prefix = $this->_config['gridPrefix'];
  673. $query = $params['query'];
  674. $options = $params['options'];
  675. $args = $query->export($this, ['keys' => ['source', 'conditions']]);
  676. $source = $args['source'];
  677. $conditions = $args['conditions'];
  678. if ($source === "{$prefix}.files") {
  679. $result = $this->_deleteFile($conditions);
  680. } else {
  681. $result = $this->connection->{$args['source']}->remove($conditions, $options);
  682. $result = $this->_ok($result);
  683. }
  684. if ($result && $query->entity()) {
  685. $query->entity()->sync(null, [], ['dematerialize' => true]);
  686. }
  687. return $result;
  688. });
  689. }
  690. protected function _deleteFile($conditions, $options = []) {
  691. $_config = $this->_config;
  692. $defaults = ['w' => $_config['w'], 'wTimeoutMS' => $_config['wTimeoutMS']];
  693. $options += $defaults;
  694. $prefix = $this->_config['gridPrefix'];
  695. return $this->connection->getGridFS($prefix)->remove($conditions, $options);
  696. }
  697. /**
  698. * Parse a `MongoCollection::<insert|update|delete>()` response and
  699. * return `true` on success.
  700. *
  701. * @return boolean
  702. */
  703. protected function _ok($result) {
  704. if (is_bool($result)) {
  705. return $result;
  706. }
  707. return !isset($result['err']) || $result['err'] === null;
  708. }
  709. /**
  710. * Executes calculation-related queries, such as those required for `count`.
  711. *
  712. * @param string $type Only accepts `count`.
  713. * @param mixed $query The query to be executed.
  714. * @param array $options Optional arguments for the `read()` query that will be executed
  715. * to obtain the calculation result.
  716. * @return integer Result of the calculation.
  717. */
  718. public function calculation($type, $query, array $options = []) {
  719. $query->calculate($type);
  720. switch ($type) {
  721. case 'count':
  722. return $this->read($query, $options)->count();
  723. }
  724. }
  725. /**
  726. * Document relationships.
  727. *
  728. * @param string $class
  729. * @param string $type Relationship type, e.g. `belongsTo`.
  730. * @param string $name
  731. * @param array $config
  732. * @return array
  733. */
  734. public function relationship($class, $type, $name, array $config = []) {
  735. $fieldName = $this->relationFieldName($type, $name);
  736. $config += compact('name', 'type', 'key', 'fieldName');
  737. $config['from'] = $class;
  738. return Libraries::instance(null, 'relationship', $config + [
  739. 'strategy' => function($rel) use ($config, $class, $name, $type) {
  740. if (isset($config['key'])) {
  741. return [];
  742. }
  743. $link = null;
  744. $hasLink = isset($config['link']);
  745. $result = [];
  746. $to = $rel->to();
  747. $local = $class::key();
  748. $className = $class::meta('name');
  749. $keys = [
  750. [$class, $name],
  751. [$class, Inflector::singularize($name)],
  752. [$to, Inflector::singularize($className)],
  753. [$to, $className]
  754. ];
  755. foreach ($keys as $map) {
  756. list($on, $key) = $map;
  757. $key = lcfirst(Inflector::camelize($key));
  758. if (!$on::hasField($key)) {
  759. continue;
  760. }
  761. $join = ($on === $class) ? [$key => $on::key()] : [$local => $key];
  762. $result['key'] = $join;
  763. if (isset($config['link'])) {
  764. return $result;
  765. }
  766. $fieldType = $on::schema()->type($key);
  767. if ($fieldType === 'id' || $fieldType === 'MongoId') {
  768. $isArray = $on::schema()->is('array', $key);
  769. $link = $isArray ? $rel::LINK_KEY_LIST : $rel::LINK_KEY;
  770. break;
  771. }
  772. }
  773. if (!$link && !$hasLink) {
  774. $link = ($type === "belongsTo") ? $rel::LINK_CONTAINED : $rel::LINK_EMBEDDED;
  775. }
  776. return $result + ($hasLink ? [] : compact('link'));
  777. }
  778. ], $this->_classes);
  779. }
  780. /**
  781. * Formats `group` clauses for MongoDB.
  782. *
  783. * @param string|array $group The group clause.
  784. * @param object $context
  785. * @return array Formatted `group` clause.
  786. */
  787. public function group($group, $context) {
  788. if (!$group) {
  789. return;
  790. }
  791. if (is_string($group) && strpos($group, 'function') === 0) {
  792. return ['$keyf' => new MongoCode($group)];
  793. }
  794. $group = (array) $group;
  795. foreach ($group as $i => $field) {
  796. if (is_int($i)) {
  797. $group[$field] = true;
  798. unset($group[$i]);
  799. }
  800. }
  801. return ['key' => $group];
  802. }
  803. /**
  804. * Maps incoming conditions with their corresponding MongoDB-native operators.
  805. *
  806. * @param array $conditions Array of conditions
  807. * @param object $context Context with which this method was called; currently
  808. * inspects the return value of `$context->type()`.
  809. * @return array Transformed conditions
  810. */
  811. public function conditions($conditions, $context) {
  812. if (!$conditions) {
  813. return [];
  814. }
  815. if ($code = $this->_isMongoCode($conditions)) {
  816. return $code;
  817. }
  818. $schema = null;
  819. $model = null;
  820. if ($context) {
  821. $schema = $context->schema();
  822. $model = $context->model();
  823. }
  824. return $this->_conditions($conditions, $model, $schema, $context);
  825. }
  826. /**
  827. * Protected helper method used to format conditions.
  828. *
  829. * @todo Catch Document/Array objects used in conditions and extract their values.
  830. * @param array $conditions The conditions array to be processed.
  831. * @param string $model The name of the model class used in the query.
  832. * @param object $schema The object containing the schema definition.
  833. * @param object $context The `Query` object.
  834. * @return array Processed query conditions.
  835. */
  836. protected function _conditions(array $conditions, $model, $schema, $context) {
  837. $ops = $this->_operators;
  838. $castOpts = [
  839. 'first' => true, 'database' => $this, 'wrap' => false, 'asContent' => true
  840. ];
  841. $cast = function($key, $value) use (&$schema, &$castOpts) {
  842. return $schema ? $schema->cast(null, $key, $value, $castOpts) : $value;
  843. };
  844. foreach ($conditions as $key => $value) {
  845. if (in_array($key, $this->_boolean)) {
  846. $operator = isset($ops[$key]) ? $ops[$key] : $key;
  847. foreach ($value as $i => $compare) {
  848. $value[$i] = $this->_conditions($compare, $model, $schema, $context);
  849. }
  850. unset($conditions[$key]);
  851. $conditions[$operator] = $value;
  852. continue;
  853. }
  854. if (is_object($value)) {
  855. continue;
  856. }
  857. if (!is_array($value)) {
  858. $conditions[$key] = $cast($key, $value);
  859. continue;
  860. }
  861. $current = key($value);
  862. if (!isset($ops[$current]) && $current[0] !== '$') {
  863. $conditions[$key] = ['$in' => $cast($key, $value)];
  864. continue;
  865. }
  866. $conditions[$key] = $this->_operators($key, $value, $schema);
  867. }
  868. return $conditions;
  869. }
  870. protected function _isMongoCode($conditions) {
  871. if (is_string($conditions)) {
  872. $conditions = new MongoCode($conditions);
  873. }
  874. if ($conditions instanceof MongoCode) {
  875. return ['$where' => $conditions];
  876. }
  877. }
  878. protected function _operators($field, $operators, $schema) {
  879. $castOpts = compact('schema');
  880. $castOpts += ['first' => true, 'database' => $this, 'wrap' => false];
  881. $cast = function($key, $value) use (&$schema, &$castOpts) {
  882. return $schema ? $schema->cast(null, $key, $value, $castOpts) : $value;
  883. };
  884. foreach ($operators as $key => $value) {
  885. if (!isset($this->_operators[$key])) {
  886. $operators[$key] = $cast($field, $value);
  887. continue;
  888. }
  889. $operator = $this->_operators[$key];
  890. if (is_array($operator)) {
  891. $operator = $operator[is_array($value) ? 'multiple' : 'single'];
  892. }
  893. if (is_callable($operator)) {
  894. return $operator($key, $value, compact('castOpts', 'field'));
  895. }
  896. unset($operators[$key]);
  897. $operators[$operator] = $cast($field, $value);
  898. }
  899. return $operators;
  900. }
  901. /**
  902. * Return formatted identifiers for fields.
  903. *
  904. * MongoDB does nt require field identifer escaping; as a result, this method is not
  905. * implemented.
  906. *
  907. * @param array $fields Fields to be parsed
  908. * @param object $context
  909. * @return array Parsed fields array
  910. */
  911. public function fields($fields, $context) {
  912. return $fields ?: [];
  913. }
  914. /**
  915. * Return formatted clause for limit.
  916. *
  917. * MongoDB doesn't require limit identifer formatting; as a result, this method is not
  918. * implemented.
  919. *
  920. * @param mixed $limit The `limit` clause to be formatted.
  921. * @param object $context The `Query` object instance.
  922. * @return mixed Formatted `limit` clause.
  923. */
  924. public function limit($limit, $context) {
  925. return $limit ?: 0;
  926. }
  927. /**
  928. * Return formatted clause for order.
  929. *
  930. * @param mixed $order The `order` clause to be formatted
  931. * @param object $context
  932. * @return mixed Formatted `order` clause.
  933. */
  934. public function order($order, $context) {
  935. if (!$order) {
  936. return [];
  937. }
  938. if (is_string($order)) {
  939. return [$order => 1];
  940. }
  941. if (!is_array($order)) {
  942. return [];
  943. }
  944. foreach ($order as $key => $value) {
  945. if (!is_string($key)) {
  946. unset($order[$key]);
  947. $order[$value] = 1;
  948. continue;
  949. }
  950. if (is_string($value)) {
  951. $order[$key] = strtolower($value) === 'asc' ? 1 : -1;
  952. }
  953. }
  954. return $order;
  955. }
  956. protected function _checkConnection() {
  957. if (!$this->_isConnected && !$this->connect()) {
  958. throw new NetworkException("Could not connect to the database.");
  959. }
  960. }
  961. /**
  962. * Returns the field name of a relation name (camelBack).
  963. *
  964. * @param string The type of the relation.
  965. * @param string The name of the relation.
  966. * @return string
  967. */
  968. public function relationFieldName($type, $name) {
  969. $fieldName = Inflector::camelize($name, false);
  970. if (preg_match('/Many$/', $type)) {
  971. $fieldName = Inflector::pluralize($fieldName);
  972. } else {
  973. $fieldName = Inflector::singularize($fieldName);
  974. }
  975. return $fieldName;
  976. }
  977. }
  978. ?>