diff --git a/extensions/mongo/Collection.php b/extensions/mongo/Collection.php index fd4f332..7292348 100644 --- a/extensions/mongo/Collection.php +++ b/extensions/mongo/Collection.php @@ -7,6 +7,7 @@ namespace yii\mongo; +use yii\base\InvalidParamException; use yii\base\Object; use Yii; @@ -32,23 +33,23 @@ class Collection extends Object } /** - * @param array $query + * @param array $condition * @param array $fields * @return \MongoCursor */ - public function find($query = [], $fields = []) + public function find($condition = [], $fields = []) { - return $this->mongoCollection->find($query, $fields); + return $this->mongoCollection->find($this->buildCondition($condition), $fields); } /** - * @param array $query + * @param array $condition * @param array $fields * @return array */ - public function findAll($query = [], $fields = []) + public function findAll($condition = [], $fields = []) { - $cursor = $this->find($query, $fields); + $cursor = $this->find($condition, $fields); $result = []; foreach ($cursor as $data) { $result[] = $data; @@ -102,19 +103,19 @@ class Collection extends Object /** * Updates the rows, which matches given criteria by given data. - * @param array $criteria description of the objects to update. + * @param array $condition description of the objects to update. * @param array $newData the object with which to update the matching records. * @param array $options list of options in format: optionName => optionValue. * @return boolean whether operation was successful. * @throws Exception on failure. */ - public function update($criteria, $newData, $options = []) + public function update($condition, $newData, $options = []) { $token = 'Updating data in ' . $this->mongoCollection->getName(); Yii::info($token, __METHOD__); try { Yii::beginProfile($token, __METHOD__); - $this->mongoCollection->update($criteria, $newData, $options); + $this->mongoCollection->update($this->buildCondition($condition), $newData, $options); Yii::endProfile($token, __METHOD__); return true; } catch (\Exception $e) { @@ -147,18 +148,18 @@ class Collection extends Object /** * Removes data from the collection. - * @param array $criteria description of records to remove. + * @param array $condition description of records to remove. * @param array $options list of options in format: optionName => optionValue. * @return boolean whether operation was successful. * @throws Exception on failure. */ - public function remove($criteria = [], $options = []) + public function remove($condition = [], $options = []) { $token = 'Removing data from ' . $this->mongoCollection->getName(); Yii::info($token, __METHOD__); try { Yii::beginProfile($token, __METHOD__); - $this->tryResultError($this->mongoCollection->remove($criteria, $options)); + $this->tryResultError($this->mongoCollection->remove($this->buildCondition($condition), $options)); Yii::endProfile($token, __METHOD__); return true; } catch (\Exception $e) { @@ -182,4 +183,68 @@ class Collection extends Object throw new Exception('Unknown error, use "w=1" option to enable error tracking'); } } + + /** + * Converts user friendly condition keyword into actual Mongo condition keyword. + * @param string $key raw condition key. + * @return string actual key. + */ + protected function normalizeConditionKeyword($key) + { + static $map = [ + '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', + '%' => '$mod', + '=' => '$$eq', + '==' => '$$eq', + 'where' => '$where' + ]; + $key = strtolower($key); + if (array_key_exists($key, $map)) { + return $map[$key]; + } 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. + */ + public function buildCondition($condition) + { + if (!is_array($condition)) { + throw new InvalidParamException('Condition should be an array.'); + } + $result = []; + foreach ($condition as $key => $value) { + if (is_array($value)) { + $actualValue = $this->buildCondition($value); + } else { + $actualValue = $value; + } + if (is_numeric($key)) { + $result[] = $actualValue; + } else { + $result[$this->normalizeConditionKeyword($key)] = $actualValue; + } + } + return $result; + } } \ No newline at end of file diff --git a/extensions/mongo/Query.php b/extensions/mongo/Query.php index 7d9920d..75bea4e 100644 --- a/extensions/mongo/Query.php +++ b/extensions/mongo/Query.php @@ -74,26 +74,72 @@ 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. */ protected function buildCursor($db = null) { - // TODO: compose query - $query = []; + $where = $this->where; + if (!is_array($where)) { + $where = []; + } $selectFields = []; if (!empty($this->select)) { foreach ($this->select as $fieldName) { $selectFields[$fieldName] = true; } } - $cursor = $this->getCollection($db)->find($query, $selectFields); + $cursor = $this->getCollection($db)->find($where, $selectFields); if (!empty($this->orderBy)) { $sort = []; foreach ($this->orderBy as $fieldName => $sortOrder) { $sort[$fieldName] = $sortOrder === SORT_DESC ? \MongoCollection::DESCENDING : \MongoCollection::ASCENDING; } - $cursor->sort($this->orderBy); + $cursor->sort($sort); } $cursor->limit($this->limit); $cursor->skip($this->offset); @@ -109,17 +155,17 @@ class Query extends Component implements QueryInterface public function all($db = null) { $cursor = $this->buildCursor($db); - if ($this->indexBy === null) { - return iterator_to_array($cursor); - } else { - $result = []; - foreach ($cursor as $row) { + $result = []; + foreach ($cursor as $row) { + if ($this->indexBy !== null) { if (is_string($this->indexBy)) { $key = $row[$this->indexBy]; } else { $key = call_user_func($this->indexBy, $row); } $result[$key] = $row; + } else { + $result[] = $row; } } return $result; diff --git a/tests/unit/extensions/mongo/QueryRunTest.php b/tests/unit/extensions/mongo/QueryRunTest.php new file mode 100644 index 0000000..b36851a --- /dev/null +++ b/tests/unit/extensions/mongo/QueryRunTest.php @@ -0,0 +1,110 @@ +setUpTestRows(); + } + + protected function tearDown() + { + $this->dropCollection('customer'); + parent::tearDown(); + } + + /** + * Sets up test rows. + */ + protected function setUpTestRows() + { + $collection = $this->getConnection()->getCollection('customer'); + $rows = []; + for ($i = 1; $i <= 10; $i++) { + $rows[] = [ + 'name' => 'name' . $i, + 'address' => 'address' . $i, + ]; + } + $collection->batchInsert($rows); + } + + // Tests : + + public function testAll() + { + $connection = $this->getConnection(); + $query = new Query; + $rows = $query->from('customer')->all($connection); + $this->assertEquals(10, count($rows)); + } + + public function testDirectMatch() + { + $connection = $this->getConnection(); + $query = new Query; + $rows = $query->from('customer') + ->where(['name' => 'name1']) + ->all($connection); + $this->assertEquals(1, count($rows)); + $this->assertEquals('name1', $rows[0]['name']); + } + + public function testIndexBy() + { + $connection = $this->getConnection(); + $query = new Query; + $rows = $query->from('customer') + ->indexBy('name') + ->all($connection); + $this->assertEquals(10, count($rows)); + $this->assertNotEmpty($rows['name1']); + } + + public function testInCondition() + { + $connection = $this->getConnection(); + $query = new Query; + $rows = $query->from('customer') + ->where([ + 'name' => [ + 'in' => ['name1', 'name5'] + ] + ]) + ->all($connection); + $this->assertEquals(2, count($rows)); + $this->assertEquals('name1', $rows[0]['name']); + $this->assertEquals('name5', $rows[1]['name']); + } + + public function testOrCondition() + { + $connection = $this->getConnection(); + $query = new Query; + $rows = $query->from('customer') + ->where(['name' => 'name1']) + ->orWhere(['address' => 'address5']) + ->all($connection); + $this->assertEquals(2, count($rows)); + $this->assertEquals('name1', $rows[0]['name']); + $this->assertEquals('address5', $rows[1]['address']); + } + + public function testOrder() + { + $connection = $this->getConnection(); + $query = new Query; + $rows = $query->from('customer') + ->orderBy(['name' => SORT_DESC]) + ->all($connection); + $this->assertEquals('name9', $rows[0]['name']); + } +} \ No newline at end of file diff --git a/tests/unit/extensions/mongo/QueryTest.php b/tests/unit/extensions/mongo/QueryTest.php index 0fce3fa..ac14fbe 100644 --- a/tests/unit/extensions/mongo/QueryTest.php +++ b/tests/unit/extensions/mongo/QueryTest.php @@ -36,6 +36,88 @@ class QueryTest extends MongoTestCase $this->assertEquals($from, $query->from); } + public function testWhere() + { + $query = new Query; + $query->where(['name' => 'name1']); + $this->assertEquals(['name' => 'name1'], $query->where); + + $query->andWhere(['address' => 'address1']); + $this->assertEquals( + [ + 'name' => 'name1', + 'address' => 'address1' + ], + $query->where + ); + + $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'] + ], + 'address' => 'address2' + ], + $query->where + ); + + $query->orWhere(['name' => 'name4']); + $this->assertEquals( + [ + 'or' => [ + [ + 'or' => [ + [ + 'name' => 'name1', + 'address' => 'address1' + ], + ['name' => 'name2'], + ['name' => 'name3'] + ], + 'address' => 'address2' + ], + ['name' => 'name4'] + ], + ], + $query->where + ); + } + public function testOrder() { $query = new Query;