QueryBuilder.php 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916
  1. <?php
  2. /**
  3. * @link http://www.yiiframework.com/
  4. * @copyright Copyright (c) 2008 Yii Software LLC
  5. * @license http://www.yiiframework.com/license/
  6. */
  7. namespace yii\mongodb;
  8. use MongoDB\BSON\Javascript;
  9. use MongoDB\BSON\ObjectID;
  10. use MongoDB\BSON\Regex;
  11. use MongoDB\Driver\Exception\InvalidArgumentException;
  12. use yii\base\InvalidParamException;
  13. use yii\base\BaseObject;
  14. use yii\helpers\ArrayHelper;
  15. /**
  16. * QueryBuilder builds a MongoDB command statements.
  17. * It is used by [[Command]] for particular commands and queries composition.
  18. *
  19. * MongoDB uses JSON format to specify query conditions with quite specific syntax.
  20. * However [[buildCondition()]] method provides the ability of "translating" common condition format used "yii\db\*"
  21. * into MongoDB condition.
  22. * For example:
  23. *
  24. * ```php
  25. * $condition = [
  26. * [
  27. * 'OR',
  28. * ['AND', ['first_name' => 'John'], ['last_name' => 'Smith']],
  29. * ['status' => [1, 2, 3]]
  30. * ],
  31. * ];
  32. * print_r(Yii::$app->mongodb->getQueryBuilder()->buildCondition($condition));
  33. * // outputs :
  34. * [
  35. * '$or' => [
  36. * [
  37. * 'first_name' => 'John',
  38. * 'last_name' => 'John',
  39. * ],
  40. * [
  41. * 'status' => ['$in' => [1, 2, 3]],
  42. * ]
  43. * ]
  44. * ]
  45. * ```
  46. *
  47. * Note: condition values for the key '_id' will be automatically cast to [[\MongoDB\BSON\ObjectID]] instance,
  48. * even if they are plain strings. However, if you have other columns, containing [[\MongoDB\BSON\ObjectID]], you
  49. * should take care of possible typecast on your own.
  50. *
  51. * @author Paul Klimov <klimov.paul@gmail.com>
  52. * @since 2.1
  53. */
  54. class QueryBuilder extends BaseObject
  55. {
  56. /**
  57. * @var Connection the MongoDB connection.
  58. */
  59. public $db;
  60. /**
  61. * Constructor.
  62. * @param Connection $connection the database connection.
  63. * @param array $config name-value pairs that will be used to initialize the object properties
  64. */
  65. public function __construct($connection, $config = [])
  66. {
  67. $this->db = $connection;
  68. parent::__construct($config);
  69. }
  70. // Commands :
  71. /**
  72. * Generates 'create collection' command.
  73. * https://docs.mongodb.com/manual/reference/method/db.createCollection/
  74. * @param string $collectionName collection name.
  75. * @param array $options collection options in format: "name" => "value"
  76. * @return array command document.
  77. */
  78. public function createCollection($collectionName, array $options = [])
  79. {
  80. $document = array_merge(['create' => $collectionName], $options);
  81. if (isset($document['indexOptionDefaults'])) {
  82. $document['indexOptionDefaults'] = (object) $document['indexOptionDefaults'];
  83. }
  84. if (isset($document['storageEngine'])) {
  85. $document['storageEngine'] = (object) $document['storageEngine'];
  86. }
  87. if (isset($document['validator'])) {
  88. $document['validator'] = (object) $document['validator'];
  89. }
  90. return $document;
  91. }
  92. /**
  93. * Generates drop database command.
  94. * https://docs.mongodb.com/manual/reference/method/db.dropDatabase/
  95. * @return array command document.
  96. */
  97. public function dropDatabase()
  98. {
  99. return ['dropDatabase' => 1];
  100. }
  101. /**
  102. * Generates drop collection command.
  103. * https://docs.mongodb.com/manual/reference/method/db.collection.drop/
  104. * @param string $collectionName name of the collection to be dropped.
  105. * @return array command document.
  106. */
  107. public function dropCollection($collectionName)
  108. {
  109. return ['drop' => $collectionName];
  110. }
  111. /**
  112. * Generates create indexes command.
  113. * @see https://docs.mongodb.com/manual/reference/method/db.collection.createIndex/
  114. * @param string|null $databaseName database name.
  115. * @param string $collectionName collection name.
  116. * @param array[] $indexes indexes specification. Each specification should be an array in format: optionName => value
  117. * The main options are:
  118. *
  119. * - keys: array, column names with sort order, to be indexed. This option is mandatory.
  120. * - unique: bool, whether to create unique index.
  121. * - name: string, the name of the index, if not set it will be generated automatically.
  122. * - background: bool, whether to bind index in the background.
  123. * - sparse: bool, whether index should reference only documents with the specified field.
  124. *
  125. * See [[https://docs.mongodb.com/manual/reference/method/db.collection.createIndex/#options-for-all-index-types]]
  126. * for the full list of options.
  127. * @return array command document.
  128. */
  129. public function createIndexes($databaseName, $collectionName, $indexes)
  130. {
  131. $normalizedIndexes = [];
  132. foreach ($indexes as $index) {
  133. if (!isset($index['key'])) {
  134. throw new InvalidParamException('"key" is required for index specification');
  135. }
  136. $index['key'] = $this->buildSortFields($index['key']);
  137. if (!isset($index['ns'])) {
  138. if ($databaseName === null) {
  139. $databaseName = $this->db->getDefaultDatabaseName();
  140. }
  141. $index['ns'] = $databaseName . '.' . $collectionName;
  142. }
  143. if (!isset($index['name'])) {
  144. $index['name'] = $this->generateIndexName($index['key']);
  145. }
  146. $normalizedIndexes[] = $index;
  147. }
  148. return [
  149. 'createIndexes' => $collectionName,
  150. 'indexes' => $normalizedIndexes,
  151. ];
  152. }
  153. /**
  154. * Generates index name for the given column orders.
  155. * Columns should be normalized using [[buildSortFields()]] before being passed to this method.
  156. * @param array $columns columns with sort order.
  157. * @return string index name.
  158. */
  159. public function generateIndexName($columns)
  160. {
  161. $parts = [];
  162. foreach ($columns as $column => $order) {
  163. $parts[] = $column . '_' . $order;
  164. }
  165. return implode('_', $parts);
  166. }
  167. /**
  168. * Generates drop indexes command.
  169. * @param string $collectionName collection name
  170. * @param string $index index name or pattern, use `*` in order to drop all indexes.
  171. * @return array command document.
  172. */
  173. public function dropIndexes($collectionName, $index)
  174. {
  175. return [
  176. 'dropIndexes' => $collectionName,
  177. 'index' => $index,
  178. ];
  179. }
  180. /**
  181. * Generates list indexes command.
  182. * @param string $collectionName collection name
  183. * @param array $options command options.
  184. * Available options are:
  185. *
  186. * - maxTimeMS: int, max execution time in ms.
  187. *
  188. * @return array command document.
  189. */
  190. public function listIndexes($collectionName, $options = [])
  191. {
  192. return array_merge(['listIndexes' => $collectionName], $options);
  193. }
  194. /**
  195. * Generates count command
  196. * @param string $collectionName
  197. * @param array $condition
  198. * @param array $options
  199. * @return array command document.
  200. */
  201. public function count($collectionName, $condition = [], $options = [])
  202. {
  203. $document = ['count' => $collectionName];
  204. if (!empty($condition)) {
  205. $document['query'] = (object) $this->buildCondition($condition);
  206. }
  207. return array_merge($document, $options);
  208. }
  209. /**
  210. * Generates 'find and modify' command.
  211. * @param string $collectionName collection name
  212. * @param array $condition filter condition
  213. * @param array $update update criteria
  214. * @param array $options list of options in format: optionName => optionValue.
  215. * @return array command document.
  216. */
  217. public function findAndModify($collectionName, $condition = [], $update = [], $options = [])
  218. {
  219. $document = array_merge(['findAndModify' => $collectionName], $options);
  220. if (!empty($condition)) {
  221. $options['query'] = $this->buildCondition($condition);
  222. }
  223. if (!empty($update)) {
  224. $options['update'] = $update;
  225. }
  226. if (isset($options['fields'])) {
  227. $options['fields'] = $this->buildSelectFields($options['fields']);
  228. }
  229. if (isset($options['sort'])) {
  230. $options['sort'] = $this->buildSortFields($options['sort']);
  231. }
  232. foreach (['fields', 'query', 'sort', 'update'] as $name) {
  233. if (isset($options[$name])) {
  234. $document[$name] = (object) $options[$name];
  235. }
  236. }
  237. return $document;
  238. }
  239. /**
  240. * Generates 'distinct' command.
  241. * @param string $collectionName collection name.
  242. * @param string $fieldName target field name.
  243. * @param array $condition filter condition
  244. * @param array $options list of options in format: optionName => optionValue.
  245. * @return array command document.
  246. */
  247. public function distinct($collectionName, $fieldName, $condition = [], $options = [])
  248. {
  249. $document = array_merge(
  250. [
  251. 'distinct' => $collectionName,
  252. 'key' => $fieldName,
  253. ],
  254. $options
  255. );
  256. if (!empty($condition)) {
  257. $document['query'] = $this->buildCondition($condition);
  258. }
  259. return $document;
  260. }
  261. /**
  262. * Generates 'group' command.
  263. * @param string $collectionName
  264. * @@param mixed $keys fields to group by. If an array or non-code object is passed,
  265. * it will be the key used to group results. If instance of [[Javascript]] passed,
  266. * it will be treated as a function that returns the key to group by.
  267. * @param array $initial Initial value of the aggregation counter object.
  268. * @param Javascript|string $reduce function that takes two arguments (the current
  269. * document and the aggregation to this point) and does the aggregation.
  270. * Argument will be automatically cast to [[Javascript]].
  271. * @param array $options optional parameters to the group command. Valid options include:
  272. * - condition - criteria for including a document in the aggregation.
  273. * - finalize - function called once per unique key that takes the final output of the reduce function.
  274. * @return array command document.
  275. */
  276. public function group($collectionName, $keys, $initial, $reduce, $options = [])
  277. {
  278. if (!($reduce instanceof Javascript)) {
  279. $reduce = new Javascript((string) $reduce);
  280. }
  281. if (isset($options['condition'])) {
  282. $options['cond'] = $this->buildCondition($options['condition']);
  283. unset($options['condition']);
  284. }
  285. if (isset($options['finalize'])) {
  286. if (!($options['finalize'] instanceof Javascript)) {
  287. $options['finalize'] = new Javascript((string) $options['finalize']);
  288. }
  289. }
  290. if (isset($options['keyf'])) {
  291. $options['$keyf'] = $options['keyf'];
  292. unset($options['keyf']);
  293. }
  294. if (isset($options['$keyf'])) {
  295. if (!($options['$keyf'] instanceof Javascript)) {
  296. $options['$keyf'] = new Javascript((string) $options['$keyf']);
  297. }
  298. }
  299. $document = [
  300. 'group' => array_merge(
  301. [
  302. 'ns' => $collectionName,
  303. 'key' => $keys,
  304. 'initial' => $initial,
  305. '$reduce' => $reduce,
  306. ],
  307. $options
  308. )
  309. ];
  310. return $document;
  311. }
  312. /**
  313. * Generates 'map-reduce' command.
  314. * @see https://docs.mongodb.com/manual/core/map-reduce/
  315. * @param string $collectionName collection name.
  316. * @param \MongoDB\BSON\Javascript|string $map function, which emits map data from collection.
  317. * Argument will be automatically cast to [[\MongoDB\BSON\Javascript]].
  318. * @param \MongoDB\BSON\Javascript|string $reduce function that takes two arguments (the map key
  319. * and the map values) and does the aggregation.
  320. * Argument will be automatically cast to [[\MongoDB\BSON\Javascript]].
  321. * @param string|array $out output collection name. It could be a string for simple output
  322. * ('outputCollection'), or an array for parametrized output (['merge' => 'outputCollection']).
  323. * You can pass ['inline' => true] to fetch the result at once without temporary collection usage.
  324. * @param array $condition filter condition for including a document in the aggregation.
  325. * @param array $options additional optional parameters to the mapReduce command. Valid options include:
  326. *
  327. * - sort: array, key to sort the input documents. The sort key must be in an existing index for this collection.
  328. * - limit: int, the maximum number of documents to return in the collection.
  329. * - finalize: \MongoDB\BSON\Javascript|string, function, which follows the reduce method and modifies the output.
  330. * - scope: array, specifies global variables that are accessible in the map, reduce and finalize functions.
  331. * - jsMode: bool, specifies whether to convert intermediate data into BSON format between the execution of the map and reduce functions.
  332. * - verbose: bool, specifies whether to include the timing information in the result information.
  333. *
  334. * @return array command document.
  335. */
  336. public function mapReduce($collectionName, $map, $reduce, $out, $condition = [], $options = [])
  337. {
  338. if (!($map instanceof Javascript)) {
  339. $map = new Javascript((string) $map);
  340. }
  341. if (!($reduce instanceof Javascript)) {
  342. $reduce = new Javascript((string) $reduce);
  343. }
  344. $document = [
  345. 'mapReduce' => $collectionName,
  346. 'map' => $map,
  347. 'reduce' => $reduce,
  348. 'out' => $out
  349. ];
  350. if (!empty($condition)) {
  351. $document['query'] = $this->buildCondition($condition);
  352. }
  353. if (!empty($options)) {
  354. $document = array_merge($document, $options);
  355. }
  356. return $document;
  357. }
  358. /**
  359. * Generates 'aggregate' command.
  360. * @param string $collectionName collection name
  361. * @param array $pipelines list of pipeline operators.
  362. * @param array $options optional parameters.
  363. * @return array command document.
  364. */
  365. public function aggregate($collectionName, $pipelines, $options = [])
  366. {
  367. foreach ($pipelines as $key => $pipeline) {
  368. if (isset($pipeline['$match'])) {
  369. $pipelines[$key]['$match'] = $this->buildCondition($pipeline['$match']);
  370. }
  371. }
  372. $document = array_merge(
  373. [
  374. 'aggregate' => $collectionName,
  375. 'pipeline' => $pipelines,
  376. 'allowDiskUse' => false,
  377. ],
  378. $options
  379. );
  380. return $document;
  381. }
  382. /**
  383. * Generates 'explain' command.
  384. * @param string $collectionName collection name.
  385. * @param array $query query options.
  386. * @return array command document.
  387. */
  388. public function explain($collectionName, $query)
  389. {
  390. $query = array_merge(
  391. ['find' => $collectionName],
  392. $query
  393. );
  394. if (isset($query['filter'])) {
  395. $query['filter'] = (object) $this->buildCondition($query['filter']);
  396. }
  397. if (isset($query['projection'])) {
  398. $query['projection'] = $this->buildSelectFields($query['projection']);
  399. }
  400. if (isset($query['sort'])) {
  401. $query['sort'] = $this->buildSortFields($query['sort']);
  402. }
  403. return [
  404. 'explain' => $query,
  405. ];
  406. }
  407. /**
  408. * Generates 'listDatabases' command.
  409. * @param array $condition filter condition.
  410. * @param array $options command options.
  411. * @return array command document.
  412. */
  413. public function listDatabases($condition = [], $options = [])
  414. {
  415. $document = array_merge(['listDatabases' => 1], $options);
  416. if (!empty($condition)) {
  417. $document['filter'] = (object)$this->buildCondition($condition);
  418. }
  419. return $document;
  420. }
  421. /**
  422. * Generates 'listCollections' command.
  423. * @param array $condition filter condition.
  424. * @param array $options command options.
  425. * @return array command document.
  426. */
  427. public function listCollections($condition = [], $options = [])
  428. {
  429. $document = array_merge(['listCollections' => 1], $options);
  430. if (!empty($condition)) {
  431. $document['filter'] = (object)$this->buildCondition($condition);
  432. }
  433. return $document;
  434. }
  435. // Service :
  436. /**
  437. * Normalizes fields list for the MongoDB select composition.
  438. * @param array|string $fields raw fields.
  439. * @return array normalized select fields.
  440. */
  441. public function buildSelectFields($fields)
  442. {
  443. $selectFields = [];
  444. foreach ((array)$fields as $key => $value) {
  445. if (is_int($key)) {
  446. $selectFields[$value] = true;
  447. } else {
  448. $selectFields[$key] = is_scalar($value) ? (bool)$value : $value;
  449. }
  450. }
  451. return $selectFields;
  452. }
  453. /**
  454. * Normalizes fields list for the MongoDB sort composition.
  455. * @param array|string $fields raw fields.
  456. * @return array normalized sort fields.
  457. */
  458. public function buildSortFields($fields)
  459. {
  460. $sortFields = [];
  461. foreach ((array)$fields as $key => $value) {
  462. if (is_int($key)) {
  463. $sortFields[$value] = +1;
  464. } else {
  465. if ($value === SORT_ASC) {
  466. $value = +1;
  467. } elseif ($value === SORT_DESC) {
  468. $value = -1;
  469. }
  470. $sortFields[$key] = $value;
  471. }
  472. }
  473. return $sortFields;
  474. }
  475. /**
  476. * Converts "\yii\db\*" quick condition keyword into actual Mongo condition keyword.
  477. * @param string $key raw condition key.
  478. * @return string actual key.
  479. */
  480. protected function normalizeConditionKeyword($key)
  481. {
  482. static $map = [
  483. 'AND' => '$and',
  484. 'OR' => '$or',
  485. 'IN' => '$in',
  486. 'NOT IN' => '$nin',
  487. ];
  488. $matchKey = strtoupper($key);
  489. if (array_key_exists($matchKey, $map)) {
  490. return $map[$matchKey];
  491. }
  492. return $key;
  493. }
  494. /**
  495. * Converts given value into [[ObjectID]] instance.
  496. * If array given, each element of it will be processed.
  497. * @param mixed $rawId raw id(s).
  498. * @return array|ObjectID normalized id(s).
  499. */
  500. protected function ensureMongoId($rawId)
  501. {
  502. if (is_array($rawId)) {
  503. $result = [];
  504. foreach ($rawId as $key => $value) {
  505. $result[$key] = $this->ensureMongoId($value);
  506. }
  507. return $result;
  508. } elseif (is_object($rawId)) {
  509. if ($rawId instanceof ObjectID) {
  510. return $rawId;
  511. } else {
  512. $rawId = (string) $rawId;
  513. }
  514. }
  515. try {
  516. $mongoId = new ObjectID($rawId);
  517. } catch (InvalidArgumentException $e) {
  518. // invalid id format
  519. $mongoId = $rawId;
  520. }
  521. return $mongoId;
  522. }
  523. /**
  524. * Parses the condition specification and generates the corresponding Mongo condition.
  525. * @param array $condition the condition specification. Please refer to [[Query::where()]]
  526. * on how to specify a condition.
  527. * @return array the generated Mongo condition
  528. * @throws InvalidParamException if the condition is in bad format
  529. */
  530. public function buildCondition($condition)
  531. {
  532. static $builders = [
  533. 'NOT' => 'buildNotCondition',
  534. 'AND' => 'buildAndCondition',
  535. 'OR' => 'buildOrCondition',
  536. 'BETWEEN' => 'buildBetweenCondition',
  537. 'NOT BETWEEN' => 'buildBetweenCondition',
  538. 'IN' => 'buildInCondition',
  539. 'NOT IN' => 'buildInCondition',
  540. 'REGEX' => 'buildRegexCondition',
  541. 'LIKE' => 'buildLikeCondition',
  542. ];
  543. if (!is_array($condition)) {
  544. throw new InvalidParamException('Condition should be an array.');
  545. } elseif (empty($condition)) {
  546. return [];
  547. }
  548. if (isset($condition[0])) { // operator format: operator, operand 1, operand 2, ...
  549. $operator = strtoupper($condition[0]);
  550. if (isset($builders[$operator])) {
  551. $method = $builders[$operator];
  552. } else {
  553. $operator = $condition[0];
  554. $method = 'buildSimpleCondition';
  555. }
  556. array_shift($condition);
  557. return $this->$method($operator, $condition);
  558. }
  559. // hash format: 'column1' => 'value1', 'column2' => 'value2', ...
  560. return $this->buildHashCondition($condition);
  561. }
  562. /**
  563. * Creates a condition based on column-value pairs.
  564. * @param array $condition the condition specification.
  565. * @return array the generated Mongo condition.
  566. */
  567. public function buildHashCondition($condition)
  568. {
  569. $result = [];
  570. foreach ($condition as $name => $value) {
  571. if (strncmp('$', $name, 1) === 0) {
  572. // Native Mongo condition:
  573. $result[$name] = $value;
  574. } else {
  575. if (is_array($value)) {
  576. if (ArrayHelper::isIndexed($value)) {
  577. // Quick IN condition:
  578. $result = array_merge($result, $this->buildInCondition('IN', [$name, $value]));
  579. } else {
  580. // Mongo complex condition:
  581. $result[$name] = $value;
  582. }
  583. } else {
  584. // Direct match:
  585. if ($name == '_id') {
  586. $value = $this->ensureMongoId($value);
  587. }
  588. $result[$name] = $value;
  589. }
  590. }
  591. }
  592. return $result;
  593. }
  594. /**
  595. * Composes `NOT` condition.
  596. * @param string $operator the operator to use for connecting the given operands
  597. * @param array $operands the Mongo conditions to connect.
  598. * @return array the generated Mongo condition.
  599. * @throws InvalidParamException if wrong number of operands have been given.
  600. */
  601. public function buildNotCondition($operator, $operands)
  602. {
  603. if (count($operands) !== 2) {
  604. throw new InvalidParamException("Operator '$operator' requires two operands.");
  605. }
  606. list($name, $value) = $operands;
  607. $result = [];
  608. if (is_array($value)) {
  609. $result[$name] = ['$not' => $this->buildCondition($value)];
  610. } else {
  611. if ($name == '_id') {
  612. $value = $this->ensureMongoId($value);
  613. }
  614. $result[$name] = ['$ne' => $value];
  615. }
  616. return $result;
  617. }
  618. /**
  619. * Connects two or more conditions with the `AND` operator.
  620. * @param string $operator the operator to use for connecting the given operands
  621. * @param array $operands the Mongo conditions to connect.
  622. * @return array the generated Mongo condition.
  623. */
  624. public function buildAndCondition($operator, $operands)
  625. {
  626. $operator = $this->normalizeConditionKeyword($operator);
  627. $parts = [];
  628. foreach ($operands as $operand) {
  629. $parts[] = $this->buildCondition($operand);
  630. }
  631. return [$operator => $parts];
  632. }
  633. /**
  634. * Connects two or more conditions with the `OR` operator.
  635. * @param string $operator the operator to use for connecting the given operands
  636. * @param array $operands the Mongo conditions to connect.
  637. * @return array the generated Mongo condition.
  638. */
  639. public function buildOrCondition($operator, $operands)
  640. {
  641. $operator = $this->normalizeConditionKeyword($operator);
  642. $parts = [];
  643. foreach ($operands as $operand) {
  644. $parts[] = $this->buildCondition($operand);
  645. }
  646. return [$operator => $parts];
  647. }
  648. /**
  649. * Creates an Mongo condition, which emulates the `BETWEEN` operator.
  650. * @param string $operator the operator to use
  651. * @param array $operands the first operand is the column name. The second and third operands
  652. * describe the interval that column value should be in.
  653. * @return array the generated Mongo condition.
  654. * @throws InvalidParamException if wrong number of operands have been given.
  655. */
  656. public function buildBetweenCondition($operator, $operands)
  657. {
  658. if (!isset($operands[0], $operands[1], $operands[2])) {
  659. throw new InvalidParamException("Operator '$operator' requires three operands.");
  660. }
  661. list($column, $value1, $value2) = $operands;
  662. if (strncmp('NOT', $operator, 3) === 0) {
  663. return [
  664. $column => [
  665. '$lt' => $value1,
  666. '$gt' => $value2,
  667. ]
  668. ];
  669. }
  670. return [
  671. $column => [
  672. '$gte' => $value1,
  673. '$lte' => $value2,
  674. ]
  675. ];
  676. }
  677. /**
  678. * Creates an Mongo condition with the `IN` operator.
  679. * @param string $operator the operator to use (e.g. `IN` or `NOT IN`)
  680. * @param array $operands the first operand is the column name. If it is an array
  681. * a composite IN condition will be generated.
  682. * The second operand is an array of values that column value should be among.
  683. * @return array the generated Mongo condition.
  684. * @throws InvalidParamException if wrong number of operands have been given.
  685. */
  686. public function buildInCondition($operator, $operands)
  687. {
  688. if (!isset($operands[0], $operands[1])) {
  689. throw new InvalidParamException("Operator '$operator' requires two operands.");
  690. }
  691. list($column, $values) = $operands;
  692. $values = (array) $values;
  693. $operator = $this->normalizeConditionKeyword($operator);
  694. if (!is_array($column)) {
  695. $columns = [$column];
  696. $values = [$column => $values];
  697. } elseif (count($column) > 1) {
  698. return $this->buildCompositeInCondition($operator, $column, $values);
  699. } else {
  700. $columns = $column;
  701. $values = [$column[0] => $values];
  702. }
  703. $result = [];
  704. foreach ($columns as $column) {
  705. if ($column == '_id') {
  706. $inValues = $this->ensureMongoId($values[$column]);
  707. } else {
  708. $inValues = $values[$column];
  709. }
  710. $inValues = array_values($inValues);
  711. if (count($inValues) === 1 && $operator === '$in') {
  712. $result[$column] = $inValues[0];
  713. } else {
  714. $result[$column][$operator] = $inValues;
  715. }
  716. }
  717. return $result;
  718. }
  719. /**
  720. * @param string $operator MongoDB the operator to use (`$in` OR `$nin`)
  721. * @param array $columns list of compare columns
  722. * @param array $values compare values in format: columnName => [values]
  723. * @return array the generated Mongo condition.
  724. */
  725. private function buildCompositeInCondition($operator, $columns, $values)
  726. {
  727. $result = [];
  728. $inValues = [];
  729. foreach ($values as $columnValues) {
  730. foreach ($columnValues as $column => $value) {
  731. if ($column == '_id') {
  732. $value = $this->ensureMongoId($value);
  733. }
  734. $inValues[$column][] = $value;
  735. }
  736. }
  737. foreach ($columns as $column) {
  738. $columnInValues = array_values($inValues[$column]);
  739. if (count($columnInValues) === 1 && $operator === '$in') {
  740. $result[$column] = $columnInValues[0];
  741. } else {
  742. $result[$column][$operator] = $columnInValues;
  743. }
  744. }
  745. return $result;
  746. }
  747. /**
  748. * Creates a Mongo regular expression condition.
  749. * @param string $operator the operator to use
  750. * @param array $operands the first operand is the column name.
  751. * The second operand is a single value that column value should be compared with.
  752. * @return array the generated Mongo condition.
  753. * @throws InvalidParamException if wrong number of operands have been given.
  754. */
  755. public function buildRegexCondition($operator, $operands)
  756. {
  757. if (!isset($operands[0], $operands[1])) {
  758. throw new InvalidParamException("Operator '$operator' requires two operands.");
  759. }
  760. list($column, $value) = $operands;
  761. if (!($value instanceof Regex)) {
  762. if (preg_match('~\/(.+)\/(.*)~', $value, $matches)) {
  763. $value = new Regex($matches[1], $matches[2]);
  764. } else {
  765. $value = new Regex($value, '');
  766. }
  767. }
  768. return [$column => $value];
  769. }
  770. /**
  771. * Creates a Mongo condition, which emulates the `LIKE` operator.
  772. * @param string $operator the operator to use
  773. * @param array $operands the first operand is the column name.
  774. * The second operand is a single value that column value should be compared with.
  775. * @return array the generated Mongo condition.
  776. * @throws InvalidParamException if wrong number of operands have been given.
  777. */
  778. public function buildLikeCondition($operator, $operands)
  779. {
  780. if (!isset($operands[0], $operands[1])) {
  781. throw new InvalidParamException("Operator '$operator' requires two operands.");
  782. }
  783. list($column, $value) = $operands;
  784. if (!($value instanceof Regex)) {
  785. $value = new Regex(preg_quote($value), 'i');
  786. }
  787. return [$column => $value];
  788. }
  789. /**
  790. * Creates an Mongo condition like `{$operator:{field:value}}`.
  791. * @param string $operator the operator to use. Besides regular MongoDB operators, aliases like `>`, `<=`,
  792. * and so on, can be used here.
  793. * @param array $operands the first operand is the column name.
  794. * The second operand is a single value that column value should be compared with.
  795. * @return string the generated Mongo condition.
  796. * @throws InvalidParamException if wrong number of operands have been given.
  797. */
  798. public function buildSimpleCondition($operator, $operands)
  799. {
  800. if (count($operands) !== 2) {
  801. throw new InvalidParamException("Operator '$operator' requires two operands.");
  802. }
  803. list($column, $value) = $operands;
  804. if (strncmp('$', $operator, 1) !== 0) {
  805. static $operatorMap = [
  806. '>' => '$gt',
  807. '<' => '$lt',
  808. '>=' => '$gte',
  809. '<=' => '$lte',
  810. '!=' => '$ne',
  811. '<>' => '$ne',
  812. '=' => '$eq',
  813. '==' => '$eq',
  814. ];
  815. if (isset($operatorMap[$operator])) {
  816. $operator = $operatorMap[$operator];
  817. } else {
  818. throw new InvalidParamException("Unsupported operator '{$operator}'");
  819. }
  820. }
  821. return [$column => [$operator => $value]];
  822. }
  823. }