diff --git a/extensions/mongo/Collection.php b/extensions/mongo/Collection.php index 17a4329..7700233 100644 --- a/extensions/mongo/Collection.php +++ b/extensions/mongo/Collection.php @@ -214,75 +214,255 @@ class Collection extends Object protected function normalizeConditionKeyword($key) { static $map = [ - 'or' => '$or', + 'OR' => '$or', '>' => '$gt', '>=' => '$gte', '<' => '$lt', '<=' => '$lte', '!=' => '$ne', '<>' => '$ne', - 'in' => '$in', - 'not in' => '$nin', - 'all' => '$all', - 'size' => '$size', - 'type' => '$type', - 'exists' => '$exists', - 'notexists' => '$exists', - 'elemmatch' => '$elemMatch', - 'mod' => '$mod', + 'IN' => '$in', + 'NOT IN' => '$nin', + 'ALL' => '$all', + 'SIZE' => '$size', + 'TYPE' => '$type', + 'EXISTS' => '$exists', + 'NOTEXISTS' => '$exists', + 'ELEMMATCH' => '$elemMatch', + 'MOD' => '$mod', '%' => '$mod', '=' => '$$eq', '==' => '$$eq', - 'where' => '$where' + 'WHERE' => '$where' ]; - $key = strtolower($key); - if (array_key_exists($key, $map)) { - return $map[$key]; + $matchKey = strtoupper($key); + if (array_key_exists($matchKey, $map)) { + return $map[$matchKey]; } else { return $key; } } /** - * Builds up Mongo condition from user friendly condition. - * @param array $condition raw condition. - * @return array normalized Mongo condition. - * @throws \yii\base\InvalidParamException on invalid condition given. + * Converts given value into [[MongoId]] instance. + * If array given, each element of it will be processed. + * @param mixed $rawId raw id(s). + * @return array|\MongoId 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 \MongoId) { + return $rawId; + } else { + $rawId = (string)$rawId; + } + } + return new \MongoId($rawId); + } + + /** + * 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 = [ + 'AND' => 'buildAndCondition', + 'OR' => 'buildOrCondition', + 'BETWEEN' => 'buildBetweenCondition', + 'NOT BETWEEN' => 'buildBetweenCondition', + 'IN' => 'buildInCondition', + 'NOT IN' => 'buildInCondition', + 'LIKE' => 'buildLikeCondition', + ]; + if (!is_array($condition)) { throw new InvalidParamException('Condition should be an array.'); + } elseif (empty($condition)) { + return []; } - $result = []; - foreach ($condition as $key => $value) { - if (is_array($value)) { - $actualValue = $this->buildCondition($value); + if (isset($condition[0])) { // operator format: operator, operand 1, operand 2, ... + $operator = strtoupper($condition[0]); + if (isset($builders[$operator])) { + $method = $builders[$operator]; + array_shift($condition); + return $this->$method($operator, $condition); } else { - $actualValue = $value; + throw new InvalidParamException('Found unknown operator in query: ' . $operator); } - if (is_numeric($key)) { - $result[] = $actualValue; + } else { + // 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) { + $name = $this->normalizeConditionKeyword($name); + if (strncmp('$', $name, 1) === 0) { + // Native Mongo condition: + $result[$name] = $value; } else { - $key = $this->normalizeConditionKeyword($key); - if (strncmp('$', $key, 1) !== 0 && is_array($actualValue) && array_key_exists(0, $actualValue)) { - // shortcut for IN condition - if ($key == '_id') { - foreach ($actualValue as &$actualValuePart) { - if (!is_object($actualValuePart)) { - $actualValuePart = new \MongoId($actualValuePart); - } + if (is_array($value)) { + if (array_key_exists(0, $value)) { + // Quick IN condition: + $result = array_merge($result, $this->buildInCondition('IN', [$name, $value])); + } else { + // Normalize possible verbose condition: + $actualValue = []; + foreach ($value as $k => $v) { + $actualValue[$this->normalizeConditionKeyword($k)] = $v; } + $result[$name] = $actualValue; } - $result[$key]['$in'] = $actualValue; } else { - if ($key == '_id' && !is_object($actualValue)) { - $actualValue = new \MongoId($actualValue); + // Direct match: + if ($name == '_id') { + $value = $this->ensureMongoId($value); } - $result[$key] = $actualValue; + $result[$name] = $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) + { + $result = []; + foreach ($operands as $operand) { + $condition = $this->buildCondition($operand); + $result = array_merge_recursive($result, $condition); + } + return $result; + } + + /** + * 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, + ] + ]; + } else { + 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; + + if (!is_array($column)) { + $columns = [$column]; + $values = [$column => $values]; + } elseif (count($column) < 2) { + $columns = $column; + $values = [$column[0] => $values]; + } else { + $columns = $column; + } + + $operator = $this->normalizeConditionKeyword($operator); + $result = []; + foreach ($columns as $column) { + if ($column == '_id') { + $inValues = $this->ensureMongoId($values[$column]); + } else { + $inValues = $values[$column]; + } + $result[$column][$operator] = $inValues; + } + return $result; + } + + /** + * 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; + return [$column => '/' . $value . '/']; + } } \ No newline at end of file diff --git a/extensions/mongo/Query.php b/extensions/mongo/Query.php index 42b3403..13be87d 100644 --- a/extensions/mongo/Query.php +++ b/extensions/mongo/Query.php @@ -74,50 +74,6 @@ class Query extends Component implements QueryInterface } /** - * Adds an additional WHERE condition to the existing one. - * The new condition and the existing one will be joined using the 'AND' operator. - * @param array $condition the new WHERE condition. Please refer to [[where()]] - * on how to specify this parameter. - * @return static the query object itself - * @see where() - * @see orWhere() - */ - public function andWhere($condition) - { - if (is_array($this->where)) { - $this->where = array_merge($this->where, $condition); - } else { - $this->where = $condition; - } - return $this; - } - - /** - * Adds an additional WHERE condition to the existing one. - * The new condition and the existing one will be joined using the 'OR' operator. - * @param array $condition the new WHERE condition. Please refer to [[where()]] - * on how to specify this parameter. - * @return static the query object itself - * @see where() - * @see andWhere() - */ - public function orWhere($condition) - { - if (is_array($this->where)) { - if (array_key_exists('or', $this->where) && count($this->where) == 1) { - $this->where['or'][] = $condition; - } else { - $this->where = [ - 'or' => [$this->where, $condition] - ]; - } - } else { - $this->where = $condition; - } - return $this; - } - - /** * @param Connection $db the database connection used to execute the query. * @return \MongoCursor mongo cursor instance. */ diff --git a/tests/unit/extensions/mongo/QueryTest.php b/tests/unit/extensions/mongo/QueryTest.php index ac14fbe..35d45e0 100644 --- a/tests/unit/extensions/mongo/QueryTest.php +++ b/tests/unit/extensions/mongo/QueryTest.php @@ -45,8 +45,9 @@ class QueryTest extends MongoTestCase $query->andWhere(['address' => 'address1']); $this->assertEquals( [ - 'name' => 'name1', - 'address' => 'address1' + 'and', + ['name' => 'name1'], + ['address' => 'address1'] ], $query->where ); @@ -54,65 +55,14 @@ class QueryTest extends MongoTestCase $query->orWhere(['name' => 'name2']); $this->assertEquals( [ - 'or' => [ - [ - 'name' => 'name1', - 'address' => 'address1' - ], - ['name' => 'name2'] - ] - ], - $query->where - ); - - $query->orWhere(['name' => 'name3']); - $this->assertEquals( - [ - 'or' => [ - [ - 'name' => 'name1', - 'address' => 'address1' - ], - ['name' => 'name2'], - ['name' => 'name3'] - ] - ], - $query->where - ); - - $query->andWhere(['address' => 'address2']); - $this->assertEquals( - [ - 'or' => [ - [ - 'name' => 'name1', - 'address' => 'address1' - ], - ['name' => 'name2'], - ['name' => 'name3'] + 'or', + [ + 'and', + ['name' => 'name1'], + ['address' => 'address1'] ], - 'address' => 'address2' - ], - $query->where - ); + ['name' => 'name2'] - $query->orWhere(['name' => 'name4']); - $this->assertEquals( - [ - 'or' => [ - [ - 'or' => [ - [ - 'name' => 'name1', - 'address' => 'address1' - ], - ['name' => 'name2'], - ['name' => 'name3'] - ], - 'address' => 'address2' - ], - ['name' => 'name4'] - ], ], $query->where );