You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
894 lines
29 KiB
894 lines
29 KiB
<?php |
|
/** |
|
* @link http://www.yiiframework.com/ |
|
* @copyright Copyright (c) 2008 Yii Software LLC |
|
* @license http://www.yiiframework.com/license/ |
|
*/ |
|
|
|
namespace yii\mongodb; |
|
|
|
use yii\base\InvalidParamException; |
|
use yii\base\Object; |
|
use Yii; |
|
use yii\helpers\Json; |
|
|
|
/** |
|
* Collection represents the Mongo collection information. |
|
* |
|
* A collection object is usually created by calling [[Database::getCollection()]] or [[Connection::getCollection()]]. |
|
* |
|
* Collection provides the basic interface for the Mongo queries, mostly: insert, update, delete operations. |
|
* For example: |
|
* |
|
* ~~~ |
|
* $collection = Yii::$app->mongo->getCollection('customer'); |
|
* $collection->insert(['name' => 'John Smith', 'status' => 1]); |
|
* ~~~ |
|
* |
|
* To perform "find" queries, please use [[Query]] instead. |
|
* |
|
* Mongo uses JSON format to specify query conditions with quite specific syntax. |
|
* However Collection class provides the ability of "translating" common condition format used "yii\db\*" |
|
* into Mongo condition. |
|
* For example: |
|
* ~~~ |
|
* $condition = [ |
|
* [ |
|
* 'OR', |
|
* ['AND', ['first_name' => 'John'], ['last_name' => 'Smith']], |
|
* ['status' => [1, 2, 3]] |
|
* ], |
|
* ]; |
|
* print_r($collection->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 [[\MongoId]] instance, |
|
* even if they are plain strings. However if you have other columns, containing [[\MongoId]], you |
|
* should take care of possible typecast on your own. |
|
* |
|
* @property string $fullName Full name of this collection, including database name. This property is |
|
* read-only. |
|
* @property array $lastError Last error information. This property is read-only. |
|
* @property string $name Name of this collection. This property is read-only. |
|
* |
|
* @author Paul Klimov <klimov.paul@gmail.com> |
|
* @since 2.0 |
|
*/ |
|
class Collection extends Object |
|
{ |
|
/** |
|
* @var \MongoCollection Mongo collection instance. |
|
*/ |
|
public $mongoCollection; |
|
|
|
/** |
|
* @return string name of this collection. |
|
*/ |
|
public function getName() |
|
{ |
|
return $this->mongoCollection->getName(); |
|
} |
|
|
|
/** |
|
* @return string full name of this collection, including database name. |
|
*/ |
|
public function getFullName() |
|
{ |
|
return $this->mongoCollection->__toString(); |
|
} |
|
|
|
/** |
|
* @return array last error information. |
|
*/ |
|
public function getLastError() |
|
{ |
|
return $this->mongoCollection->db->lastError(); |
|
} |
|
|
|
/** |
|
* Composes log/profile token. |
|
* @param string $command command name |
|
* @param array $arguments command arguments. |
|
* @return string token. |
|
*/ |
|
protected function composeLogToken($command, $arguments = []) |
|
{ |
|
$parts = []; |
|
foreach ($arguments as $argument) { |
|
$parts[] = is_scalar($argument) ? $argument : Json::encode($argument); |
|
} |
|
return $this->getFullName() . '.' . $command . '(' . implode(', ', $parts) . ')'; |
|
} |
|
|
|
/** |
|
* Drops this collection. |
|
* @throws Exception on failure. |
|
* @return boolean whether the operation successful. |
|
*/ |
|
public function drop() |
|
{ |
|
$token = $this->composeLogToken('drop'); |
|
Yii::info($token, __METHOD__); |
|
try { |
|
Yii::beginProfile($token, __METHOD__); |
|
$result = $this->mongoCollection->drop(); |
|
$this->tryResultError($result); |
|
Yii::endProfile($token, __METHOD__); |
|
return true; |
|
} catch (\Exception $e) { |
|
Yii::endProfile($token, __METHOD__); |
|
throw new Exception($e->getMessage(), (int)$e->getCode(), $e); |
|
} |
|
} |
|
|
|
/** |
|
* Creates an index on the collection and the specified fields. |
|
* @param array|string $columns column name or list of column names. |
|
* If array is given, each element in the array has as key the field name, and as |
|
* value either 1 for ascending sort, or -1 for descending sort. |
|
* You can specify field using native numeric key with the field name as a value, |
|
* in this case ascending sort will be used. |
|
* For example: |
|
* ~~~ |
|
* [ |
|
* 'name', |
|
* 'status' => -1, |
|
* ] |
|
* ~~~ |
|
* @param array $options list of options in format: optionName => optionValue. |
|
* @throws Exception on failure. |
|
* @return boolean whether the operation successful. |
|
*/ |
|
public function createIndex($columns, $options = []) |
|
{ |
|
if (!is_array($columns)) { |
|
$columns = [$columns]; |
|
} |
|
$keys = $this->normalizeIndexKeys($columns); |
|
$token = $this->composeLogToken('createIndex', [$keys, $options]); |
|
$options = array_merge(['w' => 1], $options); |
|
Yii::info($token, __METHOD__); |
|
try { |
|
Yii::beginProfile($token, __METHOD__); |
|
$result = $this->mongoCollection->ensureIndex($keys, $options); |
|
$this->tryResultError($result); |
|
Yii::endProfile($token, __METHOD__); |
|
return true; |
|
} catch (\Exception $e) { |
|
Yii::endProfile($token, __METHOD__); |
|
throw new Exception($e->getMessage(), (int)$e->getCode(), $e); |
|
} |
|
} |
|
|
|
/** |
|
* Drop indexes for specified column(s). |
|
* @param string|array $columns column name or list of column names. |
|
* If array is given, each element in the array has as key the field name, and as |
|
* value either 1 for ascending sort, or -1 for descending sort. |
|
* Use value 'text' to specify text index. |
|
* You can specify field using native numeric key with the field name as a value, |
|
* in this case ascending sort will be used. |
|
* For example: |
|
* ~~~ |
|
* [ |
|
* 'name', |
|
* 'status' => -1, |
|
* 'description' => 'text', |
|
* ] |
|
* ~~~ |
|
* @throws Exception on failure. |
|
* @return boolean whether the operation successful. |
|
*/ |
|
public function dropIndex($columns) |
|
{ |
|
if (!is_array($columns)) { |
|
$columns = [$columns]; |
|
} |
|
$keys = $this->normalizeIndexKeys($columns); |
|
$token = $this->composeLogToken('dropIndex', [$keys]); |
|
Yii::info($token, __METHOD__); |
|
try { |
|
$result = $this->mongoCollection->deleteIndex($keys); |
|
$this->tryResultError($result); |
|
return true; |
|
} catch (\Exception $e) { |
|
Yii::endProfile($token, __METHOD__); |
|
throw new Exception($e->getMessage(), (int)$e->getCode(), $e); |
|
} |
|
} |
|
|
|
/** |
|
* Compose index keys from given columns/keys list. |
|
* @param array $columns raw columns/keys list. |
|
* @return array normalizes index keys array. |
|
*/ |
|
protected function normalizeIndexKeys($columns) |
|
{ |
|
$keys = []; |
|
foreach ($columns as $key => $value) { |
|
if (is_numeric($key)) { |
|
$keys[$value] = \MongoCollection::ASCENDING; |
|
} else { |
|
$keys[$key] = $value; |
|
} |
|
} |
|
return $keys; |
|
} |
|
|
|
/** |
|
* Drops all indexes for this collection. |
|
* @throws Exception on failure. |
|
* @return integer count of dropped indexes. |
|
*/ |
|
public function dropAllIndexes() |
|
{ |
|
$token = $this->composeLogToken('dropIndexes'); |
|
Yii::info($token, __METHOD__); |
|
try { |
|
$result = $this->mongoCollection->deleteIndexes(); |
|
$this->tryResultError($result); |
|
return $result['nIndexesWas']; |
|
} catch (\Exception $e) { |
|
Yii::endProfile($token, __METHOD__); |
|
throw new Exception($e->getMessage(), (int)$e->getCode(), $e); |
|
} |
|
} |
|
|
|
/** |
|
* Returns a cursor for the search results. |
|
* In order to perform "find" queries use [[Query]] class. |
|
* @param array $condition query condition |
|
* @param array $fields fields to be selected |
|
* @return \MongoCursor cursor for the search results |
|
* @see Query |
|
*/ |
|
public function find($condition = [], $fields = []) |
|
{ |
|
return $this->mongoCollection->find($this->buildCondition($condition), $fields); |
|
} |
|
|
|
/** |
|
* Inserts new data into collection. |
|
* @param array|object $data data to be inserted. |
|
* @param array $options list of options in format: optionName => optionValue. |
|
* @return \MongoId new record id instance. |
|
* @throws Exception on failure. |
|
*/ |
|
public function insert($data, $options = []) |
|
{ |
|
$token = $this->composeLogToken('insert', [$data]); |
|
Yii::info($token, __METHOD__); |
|
try { |
|
Yii::beginProfile($token, __METHOD__); |
|
$options = array_merge(['w' => 1], $options); |
|
$this->tryResultError($this->mongoCollection->insert($data, $options)); |
|
Yii::endProfile($token, __METHOD__); |
|
return is_array($data) ? $data['_id'] : $data->_id; |
|
} catch (\Exception $e) { |
|
Yii::endProfile($token, __METHOD__); |
|
throw new Exception($e->getMessage(), (int)$e->getCode(), $e); |
|
} |
|
} |
|
|
|
/** |
|
* Inserts several new rows into collection. |
|
* @param array $rows array of arrays or objects to be inserted. |
|
* @param array $options list of options in format: optionName => optionValue. |
|
* @return array inserted data, each row will have "_id" key assigned to it. |
|
* @throws Exception on failure. |
|
*/ |
|
public function batchInsert($rows, $options = []) |
|
{ |
|
$token = $this->composeLogToken('batchInsert', [$rows]); |
|
Yii::info($token, __METHOD__); |
|
try { |
|
Yii::beginProfile($token, __METHOD__); |
|
$options = array_merge(['w' => 1], $options); |
|
$this->tryResultError($this->mongoCollection->batchInsert($rows, $options)); |
|
Yii::endProfile($token, __METHOD__); |
|
return $rows; |
|
} catch (\Exception $e) { |
|
Yii::endProfile($token, __METHOD__); |
|
throw new Exception($e->getMessage(), (int)$e->getCode(), $e); |
|
} |
|
} |
|
|
|
/** |
|
* Updates the rows, which matches given criteria by given data. |
|
* Note: for "multiple" mode Mongo requires explicit strategy "$set" or "$inc" |
|
* to be specified for the "newData". If no strategy is passed "$set" will be used. |
|
* @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 integer|boolean number of updated documents or whether operation was successful. |
|
* @throws Exception on failure. |
|
*/ |
|
public function update($condition, $newData, $options = []) |
|
{ |
|
$condition = $this->buildCondition($condition); |
|
$options = array_merge(['w' => 1, 'multiple' => true], $options); |
|
if ($options['multiple']) { |
|
$keys = array_keys($newData); |
|
if (!empty($keys) && strncmp('$', $keys[0], 1) !== 0) { |
|
$newData = ['$set' => $newData]; |
|
} |
|
} |
|
$token = $this->composeLogToken('update', [$condition, $newData, $options]); |
|
Yii::info($token, __METHOD__); |
|
try { |
|
Yii::beginProfile($token, __METHOD__); |
|
$result = $this->mongoCollection->update($condition, $newData, $options); |
|
$this->tryResultError($result); |
|
Yii::endProfile($token, __METHOD__); |
|
if (is_array($result) && array_key_exists('n', $result)) { |
|
return $result['n']; |
|
} else { |
|
return true; |
|
} |
|
} catch (\Exception $e) { |
|
Yii::endProfile($token, __METHOD__); |
|
throw new Exception($e->getMessage(), (int)$e->getCode(), $e); |
|
} |
|
} |
|
|
|
/** |
|
* Update the existing database data, otherwise insert this data |
|
* @param array|object $data data to be updated/inserted. |
|
* @param array $options list of options in format: optionName => optionValue. |
|
* @return \MongoId updated/new record id instance. |
|
* @throws Exception on failure. |
|
*/ |
|
public function save($data, $options = []) |
|
{ |
|
$token = $this->composeLogToken('save', [$data]); |
|
Yii::info($token, __METHOD__); |
|
try { |
|
Yii::beginProfile($token, __METHOD__); |
|
$options = array_merge(['w' => 1], $options); |
|
$this->tryResultError($this->mongoCollection->save($data, $options)); |
|
Yii::endProfile($token, __METHOD__); |
|
return is_array($data) ? $data['_id'] : $data->_id; |
|
} catch (\Exception $e) { |
|
Yii::endProfile($token, __METHOD__); |
|
throw new Exception($e->getMessage(), (int)$e->getCode(), $e); |
|
} |
|
} |
|
|
|
/** |
|
* Removes data from the collection. |
|
* @param array $condition description of records to remove. |
|
* @param array $options list of options in format: optionName => optionValue. |
|
* @return integer|boolean number of updated documents or whether operation was successful. |
|
* @throws Exception on failure. |
|
*/ |
|
public function remove($condition = [], $options = []) |
|
{ |
|
$condition = $this->buildCondition($condition); |
|
$options = array_merge(['w' => 1, 'multiple' => true], $options); |
|
$token = $this->composeLogToken('remove', [$condition, $options]); |
|
Yii::info($token, __METHOD__); |
|
try { |
|
Yii::beginProfile($token, __METHOD__); |
|
$result = $this->mongoCollection->remove($condition, $options); |
|
$this->tryResultError($result); |
|
Yii::endProfile($token, __METHOD__); |
|
if (is_array($result) && array_key_exists('n', $result)) { |
|
return $result['n']; |
|
} else { |
|
return true; |
|
} |
|
} catch (\Exception $e) { |
|
Yii::endProfile($token, __METHOD__); |
|
throw new Exception($e->getMessage(), (int)$e->getCode(), $e); |
|
} |
|
} |
|
|
|
/** |
|
* Returns a list of distinct values for the given column across a collection. |
|
* @param string $column column to use. |
|
* @param array $condition query parameters. |
|
* @return array|boolean array of distinct values, or "false" on failure. |
|
* @throws Exception on failure. |
|
*/ |
|
public function distinct($column, $condition = []) |
|
{ |
|
$condition = $this->buildCondition($condition); |
|
$token = $this->composeLogToken('distinct', [$column, $condition]); |
|
Yii::info($token, __METHOD__); |
|
try { |
|
Yii::beginProfile($token, __METHOD__); |
|
$result = $this->mongoCollection->distinct($column, $condition); |
|
Yii::endProfile($token, __METHOD__); |
|
return $result; |
|
} catch (\Exception $e) { |
|
Yii::endProfile($token, __METHOD__); |
|
throw new Exception($e->getMessage(), (int)$e->getCode(), $e); |
|
} |
|
} |
|
|
|
/** |
|
* Performs aggregation using Mongo Aggregation Framework. |
|
* @param array $pipeline list of pipeline operators, or just the first operator |
|
* @param array $pipelineOperator additional pipeline operator. You can specify additional |
|
* pipelines via third argument, fourth argument etc. |
|
* @return array the result of the aggregation. |
|
* @throws Exception on failure. |
|
* @see http://docs.mongodb.org/manual/applications/aggregation/ |
|
*/ |
|
public function aggregate($pipeline, $pipelineOperator = []) |
|
{ |
|
$args = func_get_args(); |
|
$token = $this->composeLogToken('aggregate', $args); |
|
Yii::info($token, __METHOD__); |
|
try { |
|
Yii::beginProfile($token, __METHOD__); |
|
$result = call_user_func_array([$this->mongoCollection, 'aggregate'], $args); |
|
$this->tryResultError($result); |
|
Yii::endProfile($token, __METHOD__); |
|
return $result['result']; |
|
} catch (\Exception $e) { |
|
Yii::endProfile($token, __METHOD__); |
|
throw new Exception($e->getMessage(), (int)$e->getCode(), $e); |
|
} |
|
} |
|
|
|
/** |
|
* Performs aggregation using Mongo "group" command. |
|
* @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 [[\MongoCode]] 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 \MongoCode|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 [[\MongoCode]]. |
|
* @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 the result of the aggregation. |
|
* @throws Exception on failure. |
|
* @see http://docs.mongodb.org/manual/reference/command/group/ |
|
*/ |
|
public function group($keys, $initial, $reduce, $options = []) |
|
{ |
|
if (!($reduce instanceof \MongoCode)) { |
|
$reduce = new \MongoCode((string)$reduce); |
|
} |
|
if (array_key_exists('condition', $options)) { |
|
$options['condition'] = $this->buildCondition($options['condition']); |
|
} |
|
if (array_key_exists('finalize', $options)) { |
|
if (!($options['finalize'] instanceof \MongoCode)) { |
|
$options['finalize'] = new \MongoCode((string)$options['finalize']); |
|
} |
|
} |
|
$token = $this->composeLogToken('group', [$keys, $initial, $reduce, $options]); |
|
Yii::info($token, __METHOD__); |
|
try { |
|
Yii::beginProfile($token, __METHOD__); |
|
// Avoid possible E_DEPRECATED for $options: |
|
if (empty($options)) { |
|
$result = $this->mongoCollection->group($keys, $initial, $reduce); |
|
} else { |
|
$result = $this->mongoCollection->group($keys, $initial, $reduce, $options); |
|
} |
|
$this->tryResultError($result); |
|
|
|
Yii::endProfile($token, __METHOD__); |
|
if (array_key_exists('retval', $result)) { |
|
return $result['retval']; |
|
} else { |
|
return []; |
|
} |
|
} catch (\Exception $e) { |
|
Yii::endProfile($token, __METHOD__); |
|
throw new Exception($e->getMessage(), (int)$e->getCode(), $e); |
|
} |
|
} |
|
|
|
/** |
|
* Performs aggregation using Mongo "map reduce" mechanism. |
|
* Note: this function will not return the aggregation result, instead it will |
|
* write it inside the another Mongo collection specified by "out" parameter. |
|
* For example: |
|
* |
|
* ~~~ |
|
* $customerCollection = Yii::$app->mongo->getCollection('customer'); |
|
* $resultCollectionName = $customerCollection->mapReduce( |
|
* 'function () {emit(this.status, this.amount)}', |
|
* 'function (key, values) {return Array.sum(values)}', |
|
* 'mapReduceOut', |
|
* ['status' => 3] |
|
* ); |
|
* $query = new Query(); |
|
* $results = $query->from($resultCollectionName)->all(); |
|
* ~~~ |
|
* |
|
* @param \MongoCode|string $map function, which emits map data from collection. |
|
* Argument will be automatically cast to [[\MongoCode]]. |
|
* @param \MongoCode|string $reduce function that takes two arguments (the map key |
|
* and the map values) and does the aggregation. |
|
* Argument will be automatically cast to [[\MongoCode]]. |
|
* @param string|array $out output collection name. It could be a string for simple output |
|
* ('outputCollection'), or an array for parametrized output (['merge' => 'outputCollection']) |
|
* @param array $condition criteria 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 - the maximum number of documents to return in the collection. |
|
* - finalize - 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 - boolean -Specifies whether to convert intermediate data into BSON format between the execution of the map and reduce functions. |
|
* - verbose - boolean - specifies whether to include the timing information in the result information. |
|
* @return string the map reduce output collection name. |
|
* @throws Exception on failure. |
|
*/ |
|
public function mapReduce($map, $reduce, $out, $condition = [], $options = []) |
|
{ |
|
if (!($map instanceof \MongoCode)) { |
|
$map = new \MongoCode((string)$map); |
|
} |
|
if (!($reduce instanceof \MongoCode)) { |
|
$reduce = new \MongoCode((string)$reduce); |
|
} |
|
$command = [ |
|
'mapReduce' => $this->getName(), |
|
'map' => $map, |
|
'reduce' => $reduce, |
|
'out' => $out |
|
]; |
|
if (!empty($condition)) { |
|
$command['query'] = $this->buildCondition($condition); |
|
} |
|
if (array_key_exists('finalize', $options)) { |
|
if (!($options['finalize'] instanceof \MongoCode)) { |
|
$options['finalize'] = new \MongoCode((string)$options['finalize']); |
|
} |
|
} |
|
if (!empty($options)) { |
|
$command = array_merge($command, $options); |
|
} |
|
$token = $this->composeLogToken('mapReduce', [$map, $reduce, $out]); |
|
Yii::info($token, __METHOD__); |
|
try { |
|
Yii::beginProfile($token, __METHOD__); |
|
$command = array_merge(['mapReduce' => $this->getName()], $command); |
|
$result = $this->mongoCollection->db->command($command); |
|
$this->tryResultError($result); |
|
Yii::endProfile($token, __METHOD__); |
|
return $result['result']; |
|
} catch (\Exception $e) { |
|
Yii::endProfile($token, __METHOD__); |
|
throw new Exception($e->getMessage(), (int)$e->getCode(), $e); |
|
} |
|
} |
|
|
|
/** |
|
* Performs full text search. |
|
* @param string $search string of terms that MongoDB parses and uses to query the text index. |
|
* @param array $condition criteria for filtering a results list. |
|
* @param array $fields list of fields to be returned in result. |
|
* @param array $options additional optional parameters to the mapReduce command. Valid options include: |
|
* - limit - the maximum number of documents to include in the response (by default 100). |
|
* - language - the language that determines the list of stop words for the search |
|
* and the rules for the stemmer and tokenizer. If not specified, the search uses the default |
|
* language of the index. |
|
* @return array the highest scoring documents, in descending order by score. |
|
* @throws Exception on failure. |
|
*/ |
|
public function fullTextSearch($search, $condition = [], $fields = [], $options = []) { |
|
$command = [ |
|
'search' => $search |
|
]; |
|
if (!empty($condition)) { |
|
$command['filter'] = $this->buildCondition($condition); |
|
} |
|
if (!empty($fields)) { |
|
$command['project'] = $fields; |
|
} |
|
if (!empty($options)) { |
|
$command = array_merge($command, $options); |
|
} |
|
$token = $this->composeLogToken('text', $command); |
|
Yii::info($token, __METHOD__); |
|
try { |
|
Yii::beginProfile($token, __METHOD__); |
|
$command = array_merge(['text' => $this->getName()], $command); |
|
$result = $this->mongoCollection->db->command($command); |
|
$this->tryResultError($result); |
|
Yii::endProfile($token, __METHOD__); |
|
return $result['results']; |
|
} catch (\Exception $e) { |
|
Yii::endProfile($token, __METHOD__); |
|
throw new Exception($e->getMessage(), (int)$e->getCode(), $e); |
|
} |
|
} |
|
|
|
/** |
|
* Checks if command execution result ended with an error. |
|
* @param mixed $result raw command execution result. |
|
* @throws Exception if an error occurred. |
|
*/ |
|
protected function tryResultError($result) |
|
{ |
|
if (is_array($result)) { |
|
if (!empty($result['errmsg'])) { |
|
$errorMessage = $result['errmsg']; |
|
} elseif (!empty($result['err'])) { |
|
$errorMessage = $result['err']; |
|
} |
|
if (isset($errorMessage)) { |
|
if (array_key_exists('code', $result)) { |
|
$errorCode = (int)$result['code']; |
|
} elseif (array_key_exists('ok', $result)) { |
|
$errorCode = (int)$result['ok']; |
|
} else { |
|
$errorCode = 0; |
|
} |
|
throw new Exception($errorMessage, $errorCode); |
|
} |
|
} elseif (!$result) { |
|
throw new Exception('Unknown error, use "w=1" option to enable error tracking'); |
|
} |
|
} |
|
|
|
/** |
|
* Throws an exception if there was an error on the last operation. |
|
* @throws Exception if an error occurred. |
|
*/ |
|
protected function tryLastError() |
|
{ |
|
$this->tryResultError($this->getLastError()); |
|
} |
|
|
|
/** |
|
* 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 = [ |
|
'OR' => '$or', |
|
'IN' => '$in', |
|
'NOT IN' => '$nin', |
|
]; |
|
$matchKey = strtoupper($key); |
|
if (array_key_exists($matchKey, $map)) { |
|
return $map[$matchKey]; |
|
} else { |
|
return $key; |
|
} |
|
} |
|
|
|
/** |
|
* 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 []; |
|
} |
|
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 { |
|
throw new InvalidParamException('Found unknown operator in query: ' . $operator); |
|
} |
|
} 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) { |
|
if (strncmp('$', $name, 1) === 0) { |
|
// Native Mongo condition: |
|
$result[$name] = $value; |
|
} else { |
|
if (is_array($value)) { |
|
if (array_key_exists(0, $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; |
|
} |
|
|
|
/** |
|
* 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; |
|
if (!($value instanceof \MongoRegex)) { |
|
$value = new \MongoRegex($value); |
|
} |
|
return [$column => $value]; |
|
} |
|
} |