Browse Source

Mongo condition composition reworked to match original DB interface.

tags/2.0.0-beta
Klimov Paul 11 years ago
parent
commit
139450dad1
  1. 254
      extensions/mongo/Collection.php
  2. 44
      extensions/mongo/Query.php
  3. 68
      tests/unit/extensions/mongo/QueryTest.php

254
extensions/mongo/Collection.php

@ -214,75 +214,255 @@ class Collection extends Object
protected function normalizeConditionKeyword($key) protected function normalizeConditionKeyword($key)
{ {
static $map = [ static $map = [
'or' => '$or', 'OR' => '$or',
'>' => '$gt', '>' => '$gt',
'>=' => '$gte', '>=' => '$gte',
'<' => '$lt', '<' => '$lt',
'<=' => '$lte', '<=' => '$lte',
'!=' => '$ne', '!=' => '$ne',
'<>' => '$ne', '<>' => '$ne',
'in' => '$in', 'IN' => '$in',
'not in' => '$nin', 'NOT IN' => '$nin',
'all' => '$all', 'ALL' => '$all',
'size' => '$size', 'SIZE' => '$size',
'type' => '$type', 'TYPE' => '$type',
'exists' => '$exists', 'EXISTS' => '$exists',
'notexists' => '$exists', 'NOTEXISTS' => '$exists',
'elemmatch' => '$elemMatch', 'ELEMMATCH' => '$elemMatch',
'mod' => '$mod', 'MOD' => '$mod',
'%' => '$mod', '%' => '$mod',
'=' => '$$eq', '=' => '$$eq',
'==' => '$$eq', '==' => '$$eq',
'where' => '$where' 'WHERE' => '$where'
]; ];
$key = strtolower($key); $matchKey = strtoupper($key);
if (array_key_exists($key, $map)) { if (array_key_exists($matchKey, $map)) {
return $map[$key]; return $map[$matchKey];
} else { } else {
return $key; return $key;
} }
} }
/** /**
* Builds up Mongo condition from user friendly condition. * Converts given value into [[MongoId]] instance.
* @param array $condition raw condition. * If array given, each element of it will be processed.
* @return array normalized Mongo condition. * @param mixed $rawId raw id(s).
* @throws \yii\base\InvalidParamException on invalid condition given. * @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) 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)) { if (!is_array($condition)) {
throw new InvalidParamException('Condition should be an array.'); throw new InvalidParamException('Condition should be an array.');
} elseif (empty($condition)) {
return [];
} }
$result = []; if (isset($condition[0])) { // operator format: operator, operand 1, operand 2, ...
foreach ($condition as $key => $value) { $operator = strtoupper($condition[0]);
if (is_array($value)) { if (isset($builders[$operator])) {
$actualValue = $this->buildCondition($value); $method = $builders[$operator];
array_shift($condition);
return $this->$method($operator, $condition);
} else { } else {
$actualValue = $value; throw new InvalidParamException('Found unknown operator in query: ' . $operator);
} }
if (is_numeric($key)) { } else {
$result[] = $actualValue; // 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 { } else {
$key = $this->normalizeConditionKeyword($key); if (is_array($value)) {
if (strncmp('$', $key, 1) !== 0 && is_array($actualValue) && array_key_exists(0, $actualValue)) { if (array_key_exists(0, $value)) {
// shortcut for IN condition // Quick IN condition:
if ($key == '_id') { $result = array_merge($result, $this->buildInCondition('IN', [$name, $value]));
foreach ($actualValue as &$actualValuePart) { } else {
if (!is_object($actualValuePart)) { // Normalize possible verbose condition:
$actualValuePart = new \MongoId($actualValuePart); $actualValue = [];
} foreach ($value as $k => $v) {
$actualValue[$this->normalizeConditionKeyword($k)] = $v;
} }
$result[$name] = $actualValue;
} }
$result[$key]['$in'] = $actualValue;
} else { } else {
if ($key == '_id' && !is_object($actualValue)) { // Direct match:
$actualValue = new \MongoId($actualValue); if ($name == '_id') {
$value = $this->ensureMongoId($value);
} }
$result[$key] = $actualValue; $result[$name] = $value;
} }
} }
} }
return $result; 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 . '/'];
}
} }

44
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. * @param Connection $db the database connection used to execute the query.
* @return \MongoCursor mongo cursor instance. * @return \MongoCursor mongo cursor instance.
*/ */

68
tests/unit/extensions/mongo/QueryTest.php

@ -45,8 +45,9 @@ class QueryTest extends MongoTestCase
$query->andWhere(['address' => 'address1']); $query->andWhere(['address' => 'address1']);
$this->assertEquals( $this->assertEquals(
[ [
'name' => 'name1', 'and',
'address' => 'address1' ['name' => 'name1'],
['address' => 'address1']
], ],
$query->where $query->where
); );
@ -54,65 +55,14 @@ class QueryTest extends MongoTestCase
$query->orWhere(['name' => 'name2']); $query->orWhere(['name' => 'name2']);
$this->assertEquals( $this->assertEquals(
[ [
'or' => [ 'or',
[ [
'name' => 'name1', 'and',
'address' => 'address1' ['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']
], ],
'address' => 'address2' ['name' => 'name2']
],
$query->where
);
$query->orWhere(['name' => 'name4']);
$this->assertEquals(
[
'or' => [
[
'or' => [
[
'name' => 'name1',
'address' => 'address1'
],
['name' => 'name2'],
['name' => 'name3']
],
'address' => 'address2'
],
['name' => 'name4']
],
], ],
$query->where $query->where
); );

Loading…
Cancel
Save