916 lines
31 KiB
PHP
916 lines
31 KiB
PHP
<?php
|
|
/**
|
|
* @link http://www.yiiframework.com/
|
|
* @copyright Copyright (c) 2008 Yii Software LLC
|
|
* @license http://www.yiiframework.com/license/
|
|
*/
|
|
|
|
namespace yii\mongodb;
|
|
|
|
use MongoDB\BSON\Javascript;
|
|
use MongoDB\BSON\ObjectID;
|
|
use MongoDB\BSON\Regex;
|
|
use MongoDB\Driver\Exception\InvalidArgumentException;
|
|
use yii\base\InvalidParamException;
|
|
use yii\base\BaseObject;
|
|
use yii\helpers\ArrayHelper;
|
|
|
|
/**
|
|
* QueryBuilder builds a MongoDB command statements.
|
|
* It is used by [[Command]] for particular commands and queries composition.
|
|
*
|
|
* MongoDB uses JSON format to specify query conditions with quite specific syntax.
|
|
* However [[buildCondition()]] method provides the ability of "translating" common condition format used "yii\db\*"
|
|
* into MongoDB condition.
|
|
* For example:
|
|
*
|
|
* ```php
|
|
* $condition = [
|
|
* [
|
|
* 'OR',
|
|
* ['AND', ['first_name' => 'John'], ['last_name' => 'Smith']],
|
|
* ['status' => [1, 2, 3]]
|
|
* ],
|
|
* ];
|
|
* print_r(Yii::$app->mongodb->getQueryBuilder()->buildCondition($condition));
|
|
* // outputs :
|
|
* [
|
|
* '$or' => [
|
|
* [
|
|
* 'first_name' => 'John',
|
|
* 'last_name' => 'John',
|
|
* ],
|
|
* [
|
|
* 'status' => ['$in' => [1, 2, 3]],
|
|
* ]
|
|
* ]
|
|
* ]
|
|
* ```
|
|
*
|
|
* Note: condition values for the key '_id' will be automatically cast to [[\MongoDB\BSON\ObjectID]] instance,
|
|
* even if they are plain strings. However, if you have other columns, containing [[\MongoDB\BSON\ObjectID]], you
|
|
* should take care of possible typecast on your own.
|
|
*
|
|
* @author Paul Klimov <klimov.paul@gmail.com>
|
|
* @since 2.1
|
|
*/
|
|
class QueryBuilder extends BaseObject
|
|
{
|
|
/**
|
|
* @var Connection the MongoDB connection.
|
|
*/
|
|
public $db;
|
|
|
|
|
|
/**
|
|
* Constructor.
|
|
* @param Connection $connection the database connection.
|
|
* @param array $config name-value pairs that will be used to initialize the object properties
|
|
*/
|
|
public function __construct($connection, $config = [])
|
|
{
|
|
$this->db = $connection;
|
|
parent::__construct($config);
|
|
}
|
|
|
|
// Commands :
|
|
|
|
/**
|
|
* Generates 'create collection' command.
|
|
* https://docs.mongodb.com/manual/reference/method/db.createCollection/
|
|
* @param string $collectionName collection name.
|
|
* @param array $options collection options in format: "name" => "value"
|
|
* @return array command document.
|
|
*/
|
|
public function createCollection($collectionName, array $options = [])
|
|
{
|
|
$document = array_merge(['create' => $collectionName], $options);
|
|
|
|
if (isset($document['indexOptionDefaults'])) {
|
|
$document['indexOptionDefaults'] = (object) $document['indexOptionDefaults'];
|
|
}
|
|
if (isset($document['storageEngine'])) {
|
|
$document['storageEngine'] = (object) $document['storageEngine'];
|
|
}
|
|
if (isset($document['validator'])) {
|
|
$document['validator'] = (object) $document['validator'];
|
|
}
|
|
|
|
return $document;
|
|
}
|
|
|
|
/**
|
|
* Generates drop database command.
|
|
* https://docs.mongodb.com/manual/reference/method/db.dropDatabase/
|
|
* @return array command document.
|
|
*/
|
|
public function dropDatabase()
|
|
{
|
|
return ['dropDatabase' => 1];
|
|
}
|
|
|
|
/**
|
|
* Generates drop collection command.
|
|
* https://docs.mongodb.com/manual/reference/method/db.collection.drop/
|
|
* @param string $collectionName name of the collection to be dropped.
|
|
* @return array command document.
|
|
*/
|
|
public function dropCollection($collectionName)
|
|
{
|
|
return ['drop' => $collectionName];
|
|
}
|
|
|
|
/**
|
|
* Generates create indexes command.
|
|
* @see https://docs.mongodb.com/manual/reference/method/db.collection.createIndex/
|
|
* @param string|null $databaseName database name.
|
|
* @param string $collectionName collection name.
|
|
* @param array[] $indexes indexes specification. Each specification should be an array in format: optionName => value
|
|
* The main options are:
|
|
*
|
|
* - keys: array, column names with sort order, to be indexed. This option is mandatory.
|
|
* - unique: bool, whether to create unique index.
|
|
* - name: string, the name of the index, if not set it will be generated automatically.
|
|
* - background: bool, whether to bind index in the background.
|
|
* - sparse: bool, whether index should reference only documents with the specified field.
|
|
*
|
|
* See [[https://docs.mongodb.com/manual/reference/method/db.collection.createIndex/#options-for-all-index-types]]
|
|
* for the full list of options.
|
|
* @return array command document.
|
|
*/
|
|
public function createIndexes($databaseName, $collectionName, $indexes)
|
|
{
|
|
$normalizedIndexes = [];
|
|
|
|
foreach ($indexes as $index) {
|
|
if (!isset($index['key'])) {
|
|
throw new InvalidParamException('"key" is required for index specification');
|
|
}
|
|
|
|
$index['key'] = $this->buildSortFields($index['key']);
|
|
|
|
if (!isset($index['ns'])) {
|
|
if ($databaseName === null) {
|
|
$databaseName = $this->db->getDefaultDatabaseName();
|
|
}
|
|
$index['ns'] = $databaseName . '.' . $collectionName;
|
|
}
|
|
|
|
if (!isset($index['name'])) {
|
|
$index['name'] = $this->generateIndexName($index['key']);
|
|
}
|
|
|
|
$normalizedIndexes[] = $index;
|
|
}
|
|
|
|
return [
|
|
'createIndexes' => $collectionName,
|
|
'indexes' => $normalizedIndexes,
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Generates index name for the given column orders.
|
|
* Columns should be normalized using [[buildSortFields()]] before being passed to this method.
|
|
* @param array $columns columns with sort order.
|
|
* @return string index name.
|
|
*/
|
|
public function generateIndexName($columns)
|
|
{
|
|
$parts = [];
|
|
foreach ($columns as $column => $order) {
|
|
$parts[] = $column . '_' . $order;
|
|
}
|
|
return implode('_', $parts);
|
|
}
|
|
|
|
/**
|
|
* Generates drop indexes command.
|
|
* @param string $collectionName collection name
|
|
* @param string $index index name or pattern, use `*` in order to drop all indexes.
|
|
* @return array command document.
|
|
*/
|
|
public function dropIndexes($collectionName, $index)
|
|
{
|
|
return [
|
|
'dropIndexes' => $collectionName,
|
|
'index' => $index,
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Generates list indexes command.
|
|
* @param string $collectionName collection name
|
|
* @param array $options command options.
|
|
* Available options are:
|
|
*
|
|
* - maxTimeMS: int, max execution time in ms.
|
|
*
|
|
* @return array command document.
|
|
*/
|
|
public function listIndexes($collectionName, $options = [])
|
|
{
|
|
return array_merge(['listIndexes' => $collectionName], $options);
|
|
}
|
|
|
|
/**
|
|
* Generates count command
|
|
* @param string $collectionName
|
|
* @param array $condition
|
|
* @param array $options
|
|
* @return array command document.
|
|
*/
|
|
public function count($collectionName, $condition = [], $options = [])
|
|
{
|
|
$document = ['count' => $collectionName];
|
|
|
|
if (!empty($condition)) {
|
|
$document['query'] = (object) $this->buildCondition($condition);
|
|
}
|
|
|
|
return array_merge($document, $options);
|
|
}
|
|
|
|
/**
|
|
* Generates 'find and modify' command.
|
|
* @param string $collectionName collection name
|
|
* @param array $condition filter condition
|
|
* @param array $update update criteria
|
|
* @param array $options list of options in format: optionName => optionValue.
|
|
* @return array command document.
|
|
*/
|
|
public function findAndModify($collectionName, $condition = [], $update = [], $options = [])
|
|
{
|
|
$document = array_merge(['findAndModify' => $collectionName], $options);
|
|
|
|
if (!empty($condition)) {
|
|
$options['query'] = $this->buildCondition($condition);
|
|
}
|
|
|
|
if (!empty($update)) {
|
|
$options['update'] = $update;
|
|
}
|
|
|
|
if (isset($options['fields'])) {
|
|
$options['fields'] = $this->buildSelectFields($options['fields']);
|
|
}
|
|
|
|
if (isset($options['sort'])) {
|
|
$options['sort'] = $this->buildSortFields($options['sort']);
|
|
}
|
|
|
|
foreach (['fields', 'query', 'sort', 'update'] as $name) {
|
|
if (isset($options[$name])) {
|
|
$document[$name] = (object) $options[$name];
|
|
}
|
|
}
|
|
|
|
return $document;
|
|
}
|
|
|
|
/**
|
|
* Generates 'distinct' command.
|
|
* @param string $collectionName collection name.
|
|
* @param string $fieldName target field name.
|
|
* @param array $condition filter condition
|
|
* @param array $options list of options in format: optionName => optionValue.
|
|
* @return array command document.
|
|
*/
|
|
public function distinct($collectionName, $fieldName, $condition = [], $options = [])
|
|
{
|
|
$document = array_merge(
|
|
[
|
|
'distinct' => $collectionName,
|
|
'key' => $fieldName,
|
|
],
|
|
$options
|
|
);
|
|
|
|
if (!empty($condition)) {
|
|
$document['query'] = $this->buildCondition($condition);
|
|
}
|
|
|
|
return $document;
|
|
}
|
|
|
|
/**
|
|
* Generates 'group' command.
|
|
* @param string $collectionName
|
|
* @@param mixed $keys fields to group by. If an array or non-code object is passed,
|
|
* it will be the key used to group results. If instance of [[Javascript]] passed,
|
|
* it will be treated as a function that returns the key to group by.
|
|
* @param array $initial Initial value of the aggregation counter object.
|
|
* @param Javascript|string $reduce function that takes two arguments (the current
|
|
* document and the aggregation to this point) and does the aggregation.
|
|
* Argument will be automatically cast to [[Javascript]].
|
|
* @param array $options optional parameters to the group command. Valid options include:
|
|
* - condition - criteria for including a document in the aggregation.
|
|
* - finalize - function called once per unique key that takes the final output of the reduce function.
|
|
* @return array command document.
|
|
*/
|
|
public function group($collectionName, $keys, $initial, $reduce, $options = [])
|
|
{
|
|
if (!($reduce instanceof Javascript)) {
|
|
$reduce = new Javascript((string) $reduce);
|
|
}
|
|
|
|
if (isset($options['condition'])) {
|
|
$options['cond'] = $this->buildCondition($options['condition']);
|
|
unset($options['condition']);
|
|
}
|
|
|
|
if (isset($options['finalize'])) {
|
|
if (!($options['finalize'] instanceof Javascript)) {
|
|
$options['finalize'] = new Javascript((string) $options['finalize']);
|
|
}
|
|
}
|
|
|
|
if (isset($options['keyf'])) {
|
|
$options['$keyf'] = $options['keyf'];
|
|
unset($options['keyf']);
|
|
}
|
|
if (isset($options['$keyf'])) {
|
|
if (!($options['$keyf'] instanceof Javascript)) {
|
|
$options['$keyf'] = new Javascript((string) $options['$keyf']);
|
|
}
|
|
}
|
|
|
|
$document = [
|
|
'group' => array_merge(
|
|
[
|
|
'ns' => $collectionName,
|
|
'key' => $keys,
|
|
'initial' => $initial,
|
|
'$reduce' => $reduce,
|
|
],
|
|
$options
|
|
)
|
|
];
|
|
|
|
return $document;
|
|
}
|
|
|
|
/**
|
|
* Generates 'map-reduce' command.
|
|
* @see https://docs.mongodb.com/manual/core/map-reduce/
|
|
* @param string $collectionName collection name.
|
|
* @param \MongoDB\BSON\Javascript|string $map function, which emits map data from collection.
|
|
* Argument will be automatically cast to [[\MongoDB\BSON\Javascript]].
|
|
* @param \MongoDB\BSON\Javascript|string $reduce function that takes two arguments (the map key
|
|
* and the map values) and does the aggregation.
|
|
* Argument will be automatically cast to [[\MongoDB\BSON\Javascript]].
|
|
* @param string|array $out output collection name. It could be a string for simple output
|
|
* ('outputCollection'), or an array for parametrized output (['merge' => 'outputCollection']).
|
|
* You can pass ['inline' => true] to fetch the result at once without temporary collection usage.
|
|
* @param array $condition filter condition for including a document in the aggregation.
|
|
* @param array $options additional optional parameters to the mapReduce command. Valid options include:
|
|
*
|
|
* - sort: array, key to sort the input documents. The sort key must be in an existing index for this collection.
|
|
* - limit: int, the maximum number of documents to return in the collection.
|
|
* - finalize: \MongoDB\BSON\Javascript|string, function, which follows the reduce method and modifies the output.
|
|
* - scope: array, specifies global variables that are accessible in the map, reduce and finalize functions.
|
|
* - jsMode: bool, specifies whether to convert intermediate data into BSON format between the execution of the map and reduce functions.
|
|
* - verbose: bool, specifies whether to include the timing information in the result information.
|
|
*
|
|
* @return array command document.
|
|
*/
|
|
public function mapReduce($collectionName, $map, $reduce, $out, $condition = [], $options = [])
|
|
{
|
|
if (!($map instanceof Javascript)) {
|
|
$map = new Javascript((string) $map);
|
|
}
|
|
if (!($reduce instanceof Javascript)) {
|
|
$reduce = new Javascript((string) $reduce);
|
|
}
|
|
|
|
$document = [
|
|
'mapReduce' => $collectionName,
|
|
'map' => $map,
|
|
'reduce' => $reduce,
|
|
'out' => $out
|
|
];
|
|
|
|
if (!empty($condition)) {
|
|
$document['query'] = $this->buildCondition($condition);
|
|
}
|
|
|
|
if (!empty($options)) {
|
|
$document = array_merge($document, $options);
|
|
}
|
|
|
|
return $document;
|
|
}
|
|
|
|
/**
|
|
* Generates 'aggregate' command.
|
|
* @param string $collectionName collection name
|
|
* @param array $pipelines list of pipeline operators.
|
|
* @param array $options optional parameters.
|
|
* @return array command document.
|
|
*/
|
|
public function aggregate($collectionName, $pipelines, $options = [])
|
|
{
|
|
foreach ($pipelines as $key => $pipeline) {
|
|
if (isset($pipeline['$match'])) {
|
|
$pipelines[$key]['$match'] = $this->buildCondition($pipeline['$match']);
|
|
}
|
|
}
|
|
|
|
$document = array_merge(
|
|
[
|
|
'aggregate' => $collectionName,
|
|
'pipeline' => $pipelines,
|
|
'allowDiskUse' => false,
|
|
],
|
|
$options
|
|
);
|
|
|
|
return $document;
|
|
}
|
|
|
|
/**
|
|
* Generates 'explain' command.
|
|
* @param string $collectionName collection name.
|
|
* @param array $query query options.
|
|
* @return array command document.
|
|
*/
|
|
public function explain($collectionName, $query)
|
|
{
|
|
$query = array_merge(
|
|
['find' => $collectionName],
|
|
$query
|
|
);
|
|
|
|
if (isset($query['filter'])) {
|
|
$query['filter'] = (object) $this->buildCondition($query['filter']);
|
|
}
|
|
if (isset($query['projection'])) {
|
|
$query['projection'] = $this->buildSelectFields($query['projection']);
|
|
}
|
|
if (isset($query['sort'])) {
|
|
$query['sort'] = $this->buildSortFields($query['sort']);
|
|
}
|
|
|
|
return [
|
|
'explain' => $query,
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Generates 'listDatabases' command.
|
|
* @param array $condition filter condition.
|
|
* @param array $options command options.
|
|
* @return array command document.
|
|
*/
|
|
public function listDatabases($condition = [], $options = [])
|
|
{
|
|
$document = array_merge(['listDatabases' => 1], $options);
|
|
if (!empty($condition)) {
|
|
$document['filter'] = (object)$this->buildCondition($condition);
|
|
}
|
|
return $document;
|
|
}
|
|
|
|
/**
|
|
* Generates 'listCollections' command.
|
|
* @param array $condition filter condition.
|
|
* @param array $options command options.
|
|
* @return array command document.
|
|
*/
|
|
public function listCollections($condition = [], $options = [])
|
|
{
|
|
$document = array_merge(['listCollections' => 1], $options);
|
|
if (!empty($condition)) {
|
|
$document['filter'] = (object)$this->buildCondition($condition);
|
|
}
|
|
return $document;
|
|
}
|
|
|
|
// Service :
|
|
|
|
/**
|
|
* Normalizes fields list for the MongoDB select composition.
|
|
* @param array|string $fields raw fields.
|
|
* @return array normalized select fields.
|
|
*/
|
|
public function buildSelectFields($fields)
|
|
{
|
|
$selectFields = [];
|
|
foreach ((array)$fields as $key => $value) {
|
|
if (is_int($key)) {
|
|
$selectFields[$value] = true;
|
|
} else {
|
|
$selectFields[$key] = is_scalar($value) ? (bool)$value : $value;
|
|
}
|
|
}
|
|
return $selectFields;
|
|
}
|
|
|
|
/**
|
|
* Normalizes fields list for the MongoDB sort composition.
|
|
* @param array|string $fields raw fields.
|
|
* @return array normalized sort fields.
|
|
*/
|
|
public function buildSortFields($fields)
|
|
{
|
|
$sortFields = [];
|
|
foreach ((array)$fields as $key => $value) {
|
|
if (is_int($key)) {
|
|
$sortFields[$value] = +1;
|
|
} else {
|
|
if ($value === SORT_ASC) {
|
|
$value = +1;
|
|
} elseif ($value === SORT_DESC) {
|
|
$value = -1;
|
|
}
|
|
$sortFields[$key] = $value;
|
|
}
|
|
}
|
|
return $sortFields;
|
|
}
|
|
|
|
/**
|
|
* Converts "\yii\db\*" quick condition keyword into actual Mongo condition keyword.
|
|
* @param string $key raw condition key.
|
|
* @return string actual key.
|
|
*/
|
|
protected function normalizeConditionKeyword($key)
|
|
{
|
|
static $map = [
|
|
'AND' => '$and',
|
|
'OR' => '$or',
|
|
'IN' => '$in',
|
|
'NOT IN' => '$nin',
|
|
];
|
|
$matchKey = strtoupper($key);
|
|
if (array_key_exists($matchKey, $map)) {
|
|
return $map[$matchKey];
|
|
}
|
|
return $key;
|
|
}
|
|
|
|
/**
|
|
* Converts given value into [[ObjectID]] instance.
|
|
* If array given, each element of it will be processed.
|
|
* @param mixed $rawId raw id(s).
|
|
* @return array|ObjectID normalized id(s).
|
|
*/
|
|
protected function ensureMongoId($rawId)
|
|
{
|
|
if (is_array($rawId)) {
|
|
$result = [];
|
|
foreach ($rawId as $key => $value) {
|
|
$result[$key] = $this->ensureMongoId($value);
|
|
}
|
|
|
|
return $result;
|
|
} elseif (is_object($rawId)) {
|
|
if ($rawId instanceof ObjectID) {
|
|
return $rawId;
|
|
} else {
|
|
$rawId = (string) $rawId;
|
|
}
|
|
}
|
|
try {
|
|
$mongoId = new ObjectID($rawId);
|
|
} catch (InvalidArgumentException $e) {
|
|
// invalid id format
|
|
$mongoId = $rawId;
|
|
}
|
|
|
|
return $mongoId;
|
|
}
|
|
|
|
/**
|
|
* Parses the condition specification and generates the corresponding Mongo condition.
|
|
* @param array $condition the condition specification. Please refer to [[Query::where()]]
|
|
* on how to specify a condition.
|
|
* @return array the generated Mongo condition
|
|
* @throws InvalidParamException if the condition is in bad format
|
|
*/
|
|
public function buildCondition($condition)
|
|
{
|
|
static $builders = [
|
|
'NOT' => 'buildNotCondition',
|
|
'AND' => 'buildAndCondition',
|
|
'OR' => 'buildOrCondition',
|
|
'BETWEEN' => 'buildBetweenCondition',
|
|
'NOT BETWEEN' => 'buildBetweenCondition',
|
|
'IN' => 'buildInCondition',
|
|
'NOT IN' => 'buildInCondition',
|
|
'REGEX' => 'buildRegexCondition',
|
|
'LIKE' => 'buildLikeCondition',
|
|
];
|
|
|
|
if (!is_array($condition)) {
|
|
throw new InvalidParamException('Condition should be an array.');
|
|
} elseif (empty($condition)) {
|
|
return [];
|
|
}
|
|
if (isset($condition[0])) { // operator format: operator, operand 1, operand 2, ...
|
|
$operator = strtoupper($condition[0]);
|
|
if (isset($builders[$operator])) {
|
|
$method = $builders[$operator];
|
|
} else {
|
|
$operator = $condition[0];
|
|
$method = 'buildSimpleCondition';
|
|
}
|
|
array_shift($condition);
|
|
return $this->$method($operator, $condition);
|
|
}
|
|
// hash format: 'column1' => 'value1', 'column2' => 'value2', ...
|
|
return $this->buildHashCondition($condition);
|
|
}
|
|
|
|
/**
|
|
* Creates a condition based on column-value pairs.
|
|
* @param array $condition the condition specification.
|
|
* @return array the generated Mongo condition.
|
|
*/
|
|
public function buildHashCondition($condition)
|
|
{
|
|
$result = [];
|
|
foreach ($condition as $name => $value) {
|
|
if (strncmp('$', $name, 1) === 0) {
|
|
// Native Mongo condition:
|
|
$result[$name] = $value;
|
|
} else {
|
|
if (is_array($value)) {
|
|
if (ArrayHelper::isIndexed($value)) {
|
|
// Quick IN condition:
|
|
$result = array_merge($result, $this->buildInCondition('IN', [$name, $value]));
|
|
} else {
|
|
// Mongo complex condition:
|
|
$result[$name] = $value;
|
|
}
|
|
} else {
|
|
// Direct match:
|
|
if ($name == '_id') {
|
|
$value = $this->ensureMongoId($value);
|
|
}
|
|
$result[$name] = $value;
|
|
}
|
|
}
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
/**
|
|
* Composes `NOT` condition.
|
|
* @param string $operator the operator to use for connecting the given operands
|
|
* @param array $operands the Mongo conditions to connect.
|
|
* @return array the generated Mongo condition.
|
|
* @throws InvalidParamException if wrong number of operands have been given.
|
|
*/
|
|
public function buildNotCondition($operator, $operands)
|
|
{
|
|
if (count($operands) !== 2) {
|
|
throw new InvalidParamException("Operator '$operator' requires two operands.");
|
|
}
|
|
|
|
list($name, $value) = $operands;
|
|
|
|
$result = [];
|
|
if (is_array($value)) {
|
|
$result[$name] = ['$not' => $this->buildCondition($value)];
|
|
} else {
|
|
if ($name == '_id') {
|
|
$value = $this->ensureMongoId($value);
|
|
}
|
|
$result[$name] = ['$ne' => $value];
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
/**
|
|
* Connects two or more conditions with the `AND` operator.
|
|
* @param string $operator the operator to use for connecting the given operands
|
|
* @param array $operands the Mongo conditions to connect.
|
|
* @return array the generated Mongo condition.
|
|
*/
|
|
public function buildAndCondition($operator, $operands)
|
|
{
|
|
$operator = $this->normalizeConditionKeyword($operator);
|
|
$parts = [];
|
|
foreach ($operands as $operand) {
|
|
$parts[] = $this->buildCondition($operand);
|
|
}
|
|
|
|
return [$operator => $parts];
|
|
}
|
|
|
|
/**
|
|
* Connects two or more conditions with the `OR` operator.
|
|
* @param string $operator the operator to use for connecting the given operands
|
|
* @param array $operands the Mongo conditions to connect.
|
|
* @return array the generated Mongo condition.
|
|
*/
|
|
public function buildOrCondition($operator, $operands)
|
|
{
|
|
$operator = $this->normalizeConditionKeyword($operator);
|
|
$parts = [];
|
|
foreach ($operands as $operand) {
|
|
$parts[] = $this->buildCondition($operand);
|
|
}
|
|
|
|
return [$operator => $parts];
|
|
}
|
|
|
|
/**
|
|
* Creates an Mongo condition, which emulates the `BETWEEN` operator.
|
|
* @param string $operator the operator to use
|
|
* @param array $operands the first operand is the column name. The second and third operands
|
|
* describe the interval that column value should be in.
|
|
* @return array the generated Mongo condition.
|
|
* @throws InvalidParamException if wrong number of operands have been given.
|
|
*/
|
|
public function buildBetweenCondition($operator, $operands)
|
|
{
|
|
if (!isset($operands[0], $operands[1], $operands[2])) {
|
|
throw new InvalidParamException("Operator '$operator' requires three operands.");
|
|
}
|
|
list($column, $value1, $value2) = $operands;
|
|
|
|
if (strncmp('NOT', $operator, 3) === 0) {
|
|
return [
|
|
$column => [
|
|
'$lt' => $value1,
|
|
'$gt' => $value2,
|
|
]
|
|
];
|
|
}
|
|
return [
|
|
$column => [
|
|
'$gte' => $value1,
|
|
'$lte' => $value2,
|
|
]
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Creates an Mongo condition with the `IN` operator.
|
|
* @param string $operator the operator to use (e.g. `IN` or `NOT IN`)
|
|
* @param array $operands the first operand is the column name. If it is an array
|
|
* a composite IN condition will be generated.
|
|
* The second operand is an array of values that column value should be among.
|
|
* @return array the generated Mongo condition.
|
|
* @throws InvalidParamException if wrong number of operands have been given.
|
|
*/
|
|
public function buildInCondition($operator, $operands)
|
|
{
|
|
if (!isset($operands[0], $operands[1])) {
|
|
throw new InvalidParamException("Operator '$operator' requires two operands.");
|
|
}
|
|
|
|
list($column, $values) = $operands;
|
|
|
|
$values = (array) $values;
|
|
$operator = $this->normalizeConditionKeyword($operator);
|
|
|
|
if (!is_array($column)) {
|
|
$columns = [$column];
|
|
$values = [$column => $values];
|
|
} elseif (count($column) > 1) {
|
|
return $this->buildCompositeInCondition($operator, $column, $values);
|
|
} else {
|
|
$columns = $column;
|
|
$values = [$column[0] => $values];
|
|
}
|
|
|
|
$result = [];
|
|
foreach ($columns as $column) {
|
|
if ($column == '_id') {
|
|
$inValues = $this->ensureMongoId($values[$column]);
|
|
} else {
|
|
$inValues = $values[$column];
|
|
}
|
|
|
|
$inValues = array_values($inValues);
|
|
if (count($inValues) === 1 && $operator === '$in') {
|
|
$result[$column] = $inValues[0];
|
|
} else {
|
|
$result[$column][$operator] = $inValues;
|
|
}
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
/**
|
|
* @param string $operator MongoDB the operator to use (`$in` OR `$nin`)
|
|
* @param array $columns list of compare columns
|
|
* @param array $values compare values in format: columnName => [values]
|
|
* @return array the generated Mongo condition.
|
|
*/
|
|
private function buildCompositeInCondition($operator, $columns, $values)
|
|
{
|
|
$result = [];
|
|
|
|
$inValues = [];
|
|
foreach ($values as $columnValues) {
|
|
foreach ($columnValues as $column => $value) {
|
|
if ($column == '_id') {
|
|
$value = $this->ensureMongoId($value);
|
|
}
|
|
$inValues[$column][] = $value;
|
|
}
|
|
}
|
|
|
|
foreach ($columns as $column) {
|
|
$columnInValues = array_values($inValues[$column]);
|
|
if (count($columnInValues) === 1 && $operator === '$in') {
|
|
$result[$column] = $columnInValues[0];
|
|
} else {
|
|
$result[$column][$operator] = $columnInValues;
|
|
}
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
/**
|
|
* Creates a Mongo regular expression condition.
|
|
* @param string $operator the operator to use
|
|
* @param array $operands the first operand is the column name.
|
|
* The second operand is a single value that column value should be compared with.
|
|
* @return array the generated Mongo condition.
|
|
* @throws InvalidParamException if wrong number of operands have been given.
|
|
*/
|
|
public function buildRegexCondition($operator, $operands)
|
|
{
|
|
if (!isset($operands[0], $operands[1])) {
|
|
throw new InvalidParamException("Operator '$operator' requires two operands.");
|
|
}
|
|
list($column, $value) = $operands;
|
|
if (!($value instanceof Regex)) {
|
|
if (preg_match('~\/(.+)\/(.*)~', $value, $matches)) {
|
|
$value = new Regex($matches[1], $matches[2]);
|
|
} else {
|
|
$value = new Regex($value, '');
|
|
}
|
|
}
|
|
|
|
return [$column => $value];
|
|
}
|
|
|
|
/**
|
|
* Creates a Mongo condition, which emulates the `LIKE` operator.
|
|
* @param string $operator the operator to use
|
|
* @param array $operands the first operand is the column name.
|
|
* The second operand is a single value that column value should be compared with.
|
|
* @return array the generated Mongo condition.
|
|
* @throws InvalidParamException if wrong number of operands have been given.
|
|
*/
|
|
public function buildLikeCondition($operator, $operands)
|
|
{
|
|
if (!isset($operands[0], $operands[1])) {
|
|
throw new InvalidParamException("Operator '$operator' requires two operands.");
|
|
}
|
|
list($column, $value) = $operands;
|
|
if (!($value instanceof Regex)) {
|
|
$value = new Regex(preg_quote($value), 'i');
|
|
}
|
|
|
|
return [$column => $value];
|
|
}
|
|
|
|
/**
|
|
* Creates an Mongo condition like `{$operator:{field:value}}`.
|
|
* @param string $operator the operator to use. Besides regular MongoDB operators, aliases like `>`, `<=`,
|
|
* and so on, can be used here.
|
|
* @param array $operands the first operand is the column name.
|
|
* The second operand is a single value that column value should be compared with.
|
|
* @return string the generated Mongo condition.
|
|
* @throws InvalidParamException if wrong number of operands have been given.
|
|
*/
|
|
public function buildSimpleCondition($operator, $operands)
|
|
{
|
|
if (count($operands) !== 2) {
|
|
throw new InvalidParamException("Operator '$operator' requires two operands.");
|
|
}
|
|
|
|
list($column, $value) = $operands;
|
|
|
|
if (strncmp('$', $operator, 1) !== 0) {
|
|
static $operatorMap = [
|
|
'>' => '$gt',
|
|
'<' => '$lt',
|
|
'>=' => '$gte',
|
|
'<=' => '$lte',
|
|
'!=' => '$ne',
|
|
'<>' => '$ne',
|
|
'=' => '$eq',
|
|
'==' => '$eq',
|
|
];
|
|
if (isset($operatorMap[$operator])) {
|
|
$operator = $operatorMap[$operator];
|
|
} else {
|
|
throw new InvalidParamException("Unsupported operator '{$operator}'");
|
|
}
|
|
}
|
|
|
|
return [$column => [$operator => $value]];
|
|
}
|
|
} |