123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635 |
- <?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\Component;
- use yii\db\QueryInterface;
- use yii\db\QueryTrait;
- use Yii;
- use yii\helpers\ArrayHelper;
- /**
- * Query represents Mongo "find" operation.
- *
- * Query provides a set of methods to facilitate the specification of "find" command.
- * These methods can be chained together.
- *
- * For example,
- *
- * ```php
- * $query = new Query();
- * // compose the query
- * $query->select(['name', 'status'])
- * ->from('customer')
- * ->limit(10);
- * // execute the query
- * $rows = $query->all();
- * ```
- *
- * @property Collection $collection Collection instance. This property is read-only.
- *
- * @author Paul Klimov <klimov.paul@gmail.com>
- * @since 2.0
- */
- class Query extends Component implements QueryInterface
- {
- use QueryTrait;
- /**
- * @var array the fields of the results to return. For example: `['name', 'group_id']`, `['name' => true, '_id' => false]`.
- * Unless directly excluded, the "_id" field is always returned. If not set, it means selecting all columns.
- * @see select()
- */
- public $select = [];
- /**
- * @var string|array the collection to be selected from. If string considered as the name of the collection
- * inside the default database. If array - first element considered as the name of the database,
- * second - as name of collection inside that database
- * @see from()
- */
- public $from;
- /**
- * @var array cursor options in format: optionKey => optionValue
- * @see \MongoDB\Driver\Cursor::addOption()
- * @see options()
- */
- public $options = [];
- /**
- * Returns the Mongo collection for this query.
- * @param Connection $db Mongo connection.
- * @return Collection collection instance.
- */
- public function getCollection($db = null)
- {
- if ($db === null) {
- $db = Yii::$app->get('mongodb');
- }
- return $db->getCollection($this->from);
- }
- /**
- * Sets the list of fields of the results to return.
- * @param array $fields fields of the results to return.
- * @return $this the query object itself.
- */
- public function select(array $fields)
- {
- $this->select = $fields;
- return $this;
- }
- /**
- * Sets the collection to be selected from.
- * @param string|array the collection to be selected from. If string considered as the name of the collection
- * inside the default database. If array - first element considered as the name of the database,
- * second - as name of collection inside that database
- * @return $this the query object itself.
- */
- public function from($collection)
- {
- $this->from = $collection;
- return $this;
- }
- /**
- * Sets the cursor options.
- * @param array $options cursor options in format: optionName => optionValue
- * @return $this the query object itself
- * @see addOptions()
- */
- public function options($options)
- {
- $this->options = $options;
- return $this;
- }
- /**
- * Adds additional cursor options.
- * @param array $options cursor options in format: optionName => optionValue
- * @return $this the query object itself
- * @see options()
- */
- public function addOptions($options)
- {
- if (is_array($this->options)) {
- $this->options = array_merge($this->options, $options);
- } else {
- $this->options = $options;
- }
- return $this;
- }
- /**
- * Helper method for easy querying on values containing some common operators.
- *
- * The comparison operator is intelligently determined based on the first few characters in the given value and
- * internally translated to a MongoDB operator.
- * In particular, it recognizes the following operators if they appear as the leading characters in the given value:
- * <: the column must be less than the given value ($lt).
- * >: the column must be greater than the given value ($gt).
- * <=: the column must be less than or equal to the given value ($lte).
- * >=: the column must be greater than or equal to the given value ($gte).
- * <>: the column must not be the same as the given value ($ne). Note that when $partialMatch is true, this would mean the value must not be a substring of the column.
- * =: the column must be equal to the given value ($eq).
- * none of the above: use the $defaultOperator
- *
- * Note that when the value is empty, no comparison expression will be added to the search condition.
- *
- * @param string $name column name
- * @param string $value column value
- * @param string $defaultOperator Defaults to =, performing an exact match.
- * For example: use 'LIKE' or 'REGEX' for partial cq regex matching
- * @see Collection::buildCondition()
- * @return $this the query object itself.
- * @since 2.0.5
- */
- public function andFilterCompare($name, $value, $defaultOperator = '=')
- {
- $matches = [];
- if (preg_match('/^(<>|>=|>|<=|<|=)/', $value, $matches)) {
- $op = $matches[1];
- $value = substr($value, strlen($op));
- } else {
- $op = $defaultOperator;
- }
- return $this->andFilterWhere([$op, $name, $value]);
- }
- /**
- * Prepares for query building.
- * This method is called before actual query composition, e.g. building cursor, count etc.
- * You may override this method to do some final preparation work before query execution.
- * @return $this a prepared query instance.
- * @since 2.1.3
- */
- public function prepare()
- {
- return $this;
- }
- /**
- * Builds the MongoDB cursor for this query.
- * @param Connection $db the MongoDB connection used to execute the query.
- * @return \MongoDB\Driver\Cursor mongo cursor instance.
- */
- public function buildCursor($db = null)
- {
- $this->prepare();
- $options = $this->options;
- if (!empty($this->orderBy)) {
- $options['sort'] = $this->orderBy;
- }
- $options['limit'] = $this->limit;
- $options['skip'] = $this->offset;
- $cursor = $this->getCollection($db)->find($this->composeCondition(), $this->select, $options);
- return $cursor;
- }
- /**
- * Fetches rows from the given Mongo cursor.
- * @param \MongoDB\Driver\Cursor $cursor Mongo cursor instance to fetch data from.
- * @param bool $all whether to fetch all rows or only first one.
- * @param string|callable $indexBy the column name or PHP callback,
- * by which the query results should be indexed by.
- * @throws Exception on failure.
- * @return array|bool result.
- */
- protected function fetchRows($cursor, $all = true, $indexBy = null)
- {
- $token = 'fetch cursor id = ' . $cursor->getId();
- Yii::info($token, __METHOD__);
- try {
- Yii::beginProfile($token, __METHOD__);
- $result = $this->fetchRowsInternal($cursor, $all);
- Yii::endProfile($token, __METHOD__);
- return $result;
- } catch (\Exception $e) {
- Yii::endProfile($token, __METHOD__);
- throw new Exception($e->getMessage(), (int) $e->getCode(), $e);
- }
- }
- /**
- * @param \MongoDB\Driver\Cursor $cursor Mongo cursor instance to fetch data from.
- * @param bool $all whether to fetch all rows or only first one.
- * @return array|bool result.
- * @see Query::fetchRows()
- */
- protected function fetchRowsInternal($cursor, $all)
- {
- $result = [];
- if ($all) {
- foreach ($cursor as $row) {
- $result[] = $row;
- }
- } else {
- if ($row = current($cursor->toArray())) {
- $result = $row;
- } else {
- $result = false;
- }
- }
- return $result;
- }
- /**
- * Starts a batch query.
- *
- * A batch query supports fetching data in batches, which can keep the memory usage under a limit.
- * This method will return a [[BatchQueryResult]] object which implements the `Iterator` interface
- * and can be traversed to retrieve the data in batches.
- *
- * For example,
- *
- * ```php
- * $query = (new Query)->from('user');
- * foreach ($query->batch() as $rows) {
- * // $rows is an array of 10 or fewer rows from user collection
- * }
- * ```
- *
- * @param int $batchSize the number of records to be fetched in each batch.
- * @param Connection $db the MongoDB connection. If not set, the "mongodb" application component will be used.
- * @return BatchQueryResult the batch query result. It implements the `Iterator` interface
- * and can be traversed to retrieve the data in batches.
- * @since 2.1
- */
- public function batch($batchSize = 100, $db = null)
- {
- return Yii::createObject([
- 'class' => BatchQueryResult::className(),
- 'query' => $this,
- 'batchSize' => $batchSize,
- 'db' => $db,
- 'each' => false,
- ]);
- }
- /**
- * Starts a batch query and retrieves data row by row.
- * This method is similar to [[batch()]] except that in each iteration of the result,
- * only one row of data is returned. For example,
- *
- * ```php
- * $query = (new Query)->from('user');
- * foreach ($query->each() as $row) {
- * }
- * ```
- *
- * @param int $batchSize the number of records to be fetched in each batch.
- * @param Connection $db the MongoDB connection. If not set, the "mongodb" application component will be used.
- * @return BatchQueryResult the batch query result. It implements the `Iterator` interface
- * and can be traversed to retrieve the data in batches.
- * @since 2.1
- */
- public function each($batchSize = 100, $db = null)
- {
- return Yii::createObject([
- 'class' => BatchQueryResult::className(),
- 'query' => $this,
- 'batchSize' => $batchSize,
- 'db' => $db,
- 'each' => true,
- ]);
- }
- /**
- * Executes the query and returns all results as an array.
- * @param Connection $db the Mongo connection used to execute the query.
- * If this parameter is not given, the `mongodb` application component will be used.
- * @return array the query results. If the query results in nothing, an empty array will be returned.
- */
- public function all($db = null)
- {
- if (!empty($this->emulateExecution)) {
- return [];
- }
- $cursor = $this->buildCursor($db);
- $rows = $this->fetchRows($cursor, true, $this->indexBy);
- return $this->populate($rows);
- }
- /**
- * Converts the raw query results into the format as specified by this query.
- * This method is internally used to convert the data fetched from database
- * into the format as required by this query.
- * @param array $rows the raw query result from database
- * @return array the converted query result
- */
- public function populate($rows)
- {
- if ($this->indexBy === null) {
- return $rows;
- }
- $result = [];
- foreach ($rows as $row) {
- $result[ArrayHelper::getValue($row, $this->indexBy)] = $row;
- }
- return $result;
- }
- /**
- * Executes the query and returns a single row of result.
- * @param Connection $db the Mongo connection used to execute the query.
- * If this parameter is not given, the `mongodb` application component will be used.
- * @return array|false the first row (in terms of an array) of the query result. `false` is returned if the query
- * results in nothing.
- */
- public function one($db = null)
- {
- if (!empty($this->emulateExecution)) {
- return false;
- }
- $cursor = $this->buildCursor($db);
- return $this->fetchRows($cursor, false);
- }
- /**
- * Returns the query result as a scalar value.
- * The value returned will be the first column in the first row of the query results.
- * Column `_id` will be automatically excluded from select fields, if [[select]] is not empty and
- * `_id` is not selected explicitly.
- * @param Connection $db the MongoDB connection used to generate the query.
- * If this parameter is not given, the `mongodb` application component will be used.
- * @return string|null|false the value of the first column in the first row of the query result.
- * `false` is returned if the query result is empty.
- * @since 2.1.2
- */
- public function scalar($db = null)
- {
- if (!empty($this->emulateExecution)) {
- return null;
- }
- $originSelect = (array)$this->select;
- if (!isset($originSelect['_id']) && array_search('_id', $originSelect, true) === false) {
- $this->select['_id'] = false;
- }
- $cursor = $this->buildCursor($db);
- $row = $this->fetchRows($cursor, false);
- if (empty($row)) {
- return false;
- }
- return reset($row);
- }
- /**
- * Executes the query and returns the first column of the result.
- * Column `_id` will be automatically excluded from select fields, if [[select]] is not empty and
- * `_id` is not selected explicitly.
- * @param Connection $db the MongoDB connection used to generate the query.
- * If this parameter is not given, the `mongodb` application component will be used.
- * @return array the first column of the query result. An empty array is returned if the query results in nothing.
- * @since 2.1.2
- */
- public function column($db = null)
- {
- if (!empty($this->emulateExecution)) {
- return [];
- }
- $originSelect = (array)$this->select;
- if (!isset($originSelect['_id']) && array_search('_id', $originSelect, true) === false) {
- $this->select['_id'] = false;
- }
- if (is_string($this->indexBy) && $originSelect && count($originSelect) === 1) {
- $this->select[] = $this->indexBy;
- }
- $cursor = $this->buildCursor($db);
- $rows = $this->fetchRows($cursor, true);
- if (empty($rows)) {
- return [];
- }
- $results = [];
- foreach ($rows as $row) {
- $value = reset($row);
- if ($this->indexBy === null) {
- $results[] = $value;
- } else {
- if ($this->indexBy instanceof \Closure) {
- $results[call_user_func($this->indexBy, $row)] = $value;
- } else {
- $results[$row[$this->indexBy]] = $value;
- }
- }
- }
- return $results;
- }
- /**
- * Performs 'findAndModify' query and returns a single row of result.
- * @param array $update update criteria
- * @param array $options list of options in format: optionName => optionValue.
- * @param Connection $db the Mongo connection used to execute the query.
- * @return array|null the original document, or the modified document when $options['new'] is set.
- */
- public function modify($update, $options = [], $db = null)
- {
- if (!empty($this->emulateExecution)) {
- return null;
- }
- $this->prepare();
- $collection = $this->getCollection($db);
- if (!empty($this->orderBy)) {
- $options['sort'] = $this->orderBy;
- }
- $options['fields'] = $this->select;
- return $collection->findAndModify($this->composeCondition(), $update, $options);
- }
- /**
- * Returns the number of records.
- * @param string $q kept to match [[QueryInterface]], its value is ignored.
- * @param Connection $db the Mongo connection used to execute the query.
- * If this parameter is not given, the `mongodb` application component will be used.
- * @return int number of records
- * @throws Exception on failure.
- */
- public function count($q = '*', $db = null)
- {
- if (!empty($this->emulateExecution)) {
- return 0;
- }
- $this->prepare();
- $collection = $this->getCollection($db);
- return $collection->count($this->where, $this->options);
- }
- /**
- * Returns a value indicating whether the query result contains any row of data.
- * @param Connection $db the Mongo connection used to execute the query.
- * If this parameter is not given, the `mongodb` application component will be used.
- * @return bool whether the query result contains any row of data.
- */
- public function exists($db = null)
- {
- if (!empty($this->emulateExecution)) {
- return false;
- }
- $cursor = $this->buildCursor($db);
- foreach ($cursor as $row) {
- return true;
- }
- return false;
- }
- /**
- * Returns the sum of the specified column values.
- * @param string $q the column name.
- * Make sure you properly quote column names in the expression.
- * @param Connection $db the Mongo connection used to execute the query.
- * If this parameter is not given, the `mongodb` application component will be used.
- * @return int the sum of the specified column values
- */
- public function sum($q, $db = null)
- {
- if (!empty($this->emulateExecution)) {
- return 0;
- }
- return $this->aggregate($q, 'sum', $db);
- }
- /**
- * Returns the average of the specified column values.
- * @param string $q the column name.
- * Make sure you properly quote column names in the expression.
- * @param Connection $db the Mongo connection used to execute the query.
- * If this parameter is not given, the `mongodb` application component will be used.
- * @return int the average of the specified column values.
- */
- public function average($q, $db = null)
- {
- if (!empty($this->emulateExecution)) {
- return 0;
- }
- return $this->aggregate($q, 'avg', $db);
- }
- /**
- * Returns the minimum of the specified column values.
- * @param string $q the column name.
- * Make sure you properly quote column names in the expression.
- * @param Connection $db the MongoDB connection used to execute the query.
- * If this parameter is not given, the `db` application component will be used.
- * @return int the minimum of the specified column values.
- */
- public function min($q, $db = null)
- {
- return $this->aggregate($q, 'min', $db);
- }
- /**
- * Returns the maximum of the specified column values.
- * @param string $q the column name.
- * Make sure you properly quote column names in the expression.
- * @param Connection $db the MongoDB connection used to execute the query.
- * If this parameter is not given, the `mongodb` application component will be used.
- * @return int the maximum of the specified column values.
- */
- public function max($q, $db = null)
- {
- return $this->aggregate($q, 'max', $db);
- }
- /**
- * Performs the aggregation for the given column.
- * @param string $column column name.
- * @param string $operator aggregation operator.
- * @param Connection $db the database connection used to execute the query.
- * @return int aggregation result.
- */
- protected function aggregate($column, $operator, $db)
- {
- if (!empty($this->emulateExecution)) {
- return null;
- }
- $this->prepare();
- $collection = $this->getCollection($db);
- $pipelines = [];
- if ($this->where !== null) {
- $pipelines[] = ['$match' => $this->where];
- }
- $pipelines[] = [
- '$group' => [
- '_id' => '1',
- 'total' => [
- '$' . $operator => '$' . $column
- ],
- ]
- ];
- $result = $collection->aggregate($pipelines);
- if (array_key_exists(0, $result)) {
- return $result[0]['total'];
- }
- return null;
- }
- /**
- * Returns a list of distinct values for the given column across a collection.
- * @param string $q column to use.
- * @param Connection $db the MongoDB connection used to execute the query.
- * If this parameter is not given, the `mongodb` application component will be used.
- * @return array array of distinct values
- */
- public function distinct($q, $db = null)
- {
- if (!empty($this->emulateExecution)) {
- return [];
- }
- $this->prepare();
- $collection = $this->getCollection($db);
- if ($this->where !== null) {
- $condition = $this->where;
- } else {
- $condition = [];
- }
- $result = $collection->distinct($q, $condition);
- if ($result === false) {
- return [];
- }
- return $result;
- }
- /**
- * Composes condition from raw [[where]] value.
- * @return array conditions.
- */
- private function composeCondition()
- {
- if ($this->where === null) {
- return [];
- }
- return $this->where;
- }
- }
|