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)
{
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 . '/'];
}
}

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.
* @return \MongoCursor mongo cursor instance.
*/

68
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
);

Loading…
Cancel
Save