Query.php 13.1 KB
Newer Older
1 2 3 4 5 6 7
<?php
/**
 * @link http://www.yiiframework.com/
 * @copyright Copyright (c) 2008 Yii Software LLC
 * @license http://www.yiiframework.com/license/
 */

8
namespace yii\mongodb;
9 10 11

use yii\base\Component;
use yii\db\QueryInterface;
12
use yii\db\QueryTrait;
13
use yii\helpers\Json;
14
use Yii;
15 16

/**
17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
 * 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,
 *
 * ~~~
 * $query = new Query;
 * // compose the query
 * $query->select(['name', 'status'])
 *     ->from('customer')
 *     ->limit(10);
 * // execute the query
 * $rows = $query->all();
 * ~~~
33
 *
Qiang Xue committed
34 35
 * @property Collection $collection Collection instance. This property is read-only.
 *
36 37 38 39 40
 * @author Paul Klimov <klimov.paul@gmail.com>
 * @since 2.0
 */
class Query extends Component implements QueryInterface
{
41
    use QueryTrait;
42

43
    /**
44 45
     * @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.
46 47 48 49
     * @see select()
     */
    public $select = [];
    /**
50
     * @var string|array the collection to be selected from. If string considered as the name of the collection
51 52 53 54 55
     * 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;
56

57

58 59
    /**
     * Returns the Mongo collection for this query.
60
     * @param Connection $db Mongo connection.
61 62 63 64 65
     * @return Collection collection instance.
     */
    public function getCollection($db = null)
    {
        if ($db === null) {
66
            $db = Yii::$app->get('mongodb');
67
        }
68

69 70
        return $db->getCollection($this->from);
    }
71

72 73
    /**
     * Sets the list of fields of the results to return.
74
     * @param array $fields fields of the results to return.
75 76 77 78 79
     * @return static the query object itself.
     */
    public function select(array $fields)
    {
        $this->select = $fields;
80

81 82
        return $this;
    }
83

84 85
    /**
     * Sets the collection to be selected from.
86
     * @param string|array the collection to be selected from. If string considered as the name of the collection
87 88 89 90 91 92 93
     * inside the default database. If array - first element considered as the name of the database,
     * second - as name of collection inside that database
     * @return static the query object itself.
     */
    public function from($collection)
    {
        $this->from = $collection;
94

95 96
        return $this;
    }
97

98 99
    /**
     * Builds the Mongo cursor for this query.
100
     * @param Connection $db the database connection used to execute the query.
101 102 103 104
     * @return \MongoCursor mongo cursor instance.
     */
    protected function buildCursor($db = null)
    {
105
        $cursor = $this->getCollection($db)->find($this->composeCondition(), $this->composeSelectFields());
106
        if (!empty($this->orderBy)) {
107
            $cursor->sort($this->composeSort());
108 109 110
        }
        $cursor->limit($this->limit);
        $cursor->skip($this->offset);
111

112 113
        return $cursor;
    }
114

115 116
    /**
     * Fetches rows from the given Mongo cursor.
117 118 119 120 121 122
     * @param \MongoCursor $cursor Mongo cursor instance to fetch data from.
     * @param boolean $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|boolean result.
123 124 125 126 127 128 129 130 131
     */
    protected function fetchRows($cursor, $all = true, $indexBy = null)
    {
        $token = 'find(' . Json::encode($cursor->info()) . ')';
        Yii::info($token, __METHOD__);
        try {
            Yii::beginProfile($token, __METHOD__);
            $result = $this->fetchRowsInternal($cursor, $all, $indexBy);
            Yii::endProfile($token, __METHOD__);
132

133 134 135 136 137 138
            return $result;
        } catch (\Exception $e) {
            Yii::endProfile($token, __METHOD__);
            throw new Exception($e->getMessage(), (int) $e->getCode(), $e);
        }
    }
139

140
    /**
141 142 143 144
     * @param \MongoCursor $cursor Mongo cursor instance to fetch data from.
     * @param boolean $all whether to fetch all rows or only first one.
     * @param string|callable $indexBy value to index by.
     * @return array|boolean result.
145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169
     * @see Query::fetchRows()
     */
    protected function fetchRowsInternal($cursor, $all, $indexBy)
    {
        $result = [];
        if ($all) {
            foreach ($cursor as $row) {
                if ($indexBy !== null) {
                    if (is_string($indexBy)) {
                        $key = $row[$indexBy];
                    } else {
                        $key = call_user_func($indexBy, $row);
                    }
                    $result[$key] = $row;
                } else {
                    $result[] = $row;
                }
            }
        } else {
            if ($cursor->hasNext()) {
                $result = $cursor->getNext();
            } else {
                $result = false;
            }
        }
170

171 172
        return $result;
    }
173

174 175
    /**
     * Executes the query and returns all results as an array.
176 177 178
     * @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.
179 180 181 182
     */
    public function all($db = null)
    {
        $cursor = $this->buildCursor($db);
183

184 185
        return $this->fetchRows($cursor, true, $this->indexBy);
    }
186

187 188
    /**
     * Executes the query and returns a single row of result.
189 190
     * @param Connection $db the Mongo connection used to execute the query.
     * If this parameter is not given, the `mongodb` application component will be used.
191
     * @return array|boolean the first row (in terms of an array) of the query result. False is returned if the query
192
     * results in nothing.
193 194 195 196
     */
    public function one($db = null)
    {
        $cursor = $this->buildCursor($db);
197

198 199 200
        return $this->fetchRows($cursor, false);
    }

201 202 203 204 205 206 207
    /**
     * 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.
     */
208
    public function modify($update, $options = [], $db = null)
209 210 211 212 213 214 215 216 217
    {
        $collection = $this->getCollection($db);
        if (!empty($this->orderBy)) {
            $options['sort'] = $this->composeSort();
        }

        return $collection->findAndModify($this->composeCondition(), $update, $this->composeSelectFields(), $options);
    }

218 219
    /**
     * Returns the number of records.
220 221 222 223 224
     * @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 integer number of records
     * @throws Exception on failure.
225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244
     */
    public function count($q = '*', $db = null)
    {
        $cursor = $this->buildCursor($db);
        $token = 'find.count(' . Json::encode($cursor->info()) . ')';
        Yii::info($token, __METHOD__);
        try {
            Yii::beginProfile($token, __METHOD__);
            $result = $cursor->count();
            Yii::endProfile($token, __METHOD__);

            return $result;
        } catch (\Exception $e) {
            Yii::endProfile($token, __METHOD__);
            throw new Exception($e->getMessage(), (int) $e->getCode(), $e);
        }
    }

    /**
     * Returns a value indicating whether the query result contains any row of data.
245 246 247
     * @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 boolean whether the query result contains any row of data.
248 249 250 251 252 253 254 255
     */
    public function exists($db = null)
    {
        return $this->one($db) !== null;
    }

    /**
     * Returns the sum of the specified column values.
256 257 258 259 260
     * @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 integer the sum of the specified column values
261 262 263 264 265 266 267 268
     */
    public function sum($q, $db = null)
    {
        return $this->aggregate($q, 'sum', $db);
    }

    /**
     * Returns the average of the specified column values.
269 270 271 272 273
     * @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 integer the average of the specified column values.
274 275 276 277 278 279 280 281
     */
    public function average($q, $db = null)
    {
        return $this->aggregate($q, 'avg', $db);
    }

    /**
     * Returns the minimum of the specified column values.
282 283 284 285 286
     * @param string $q the column name.
     * Make sure you properly quote column names in the expression.
     * @param Connection $db the database connection used to generate the SQL statement.
     * If this parameter is not given, the `db` application component will be used.
     * @return integer the minimum of the specified column values.
287 288 289 290 291 292 293 294
     */
    public function min($q, $db = null)
    {
        return $this->aggregate($q, 'min', $db);
    }

    /**
     * Returns the maximum of the specified column values.
295 296 297 298 299
     * @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 integer the maximum of the specified column values.
300 301 302 303 304 305 306 307
     */
    public function max($q, $db = null)
    {
        return $this->aggregate($q, 'max', $db);
    }

    /**
     * Performs the aggregation for the given column.
308 309 310 311
     * @param string $column column name.
     * @param string $operator aggregation operator.
     * @param Connection $db the database connection used to execute the query.
     * @return integer aggregation result.
312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337
     */
    protected function aggregate($column, $operator, $db)
    {
        $collection = $this->getCollection($db);
        $pipelines = [];
        if ($this->where !== null) {
            $pipelines[] = ['$match' => $collection->buildCondition($this->where)];
        }
        $pipelines[] = [
            '$group' => [
                '_id' => '1',
                'total' => [
                    '$' . $operator => '$' . $column
                ],
            ]
        ];
        $result = $collection->aggregate($pipelines);
        if (array_key_exists(0, $result)) {
            return $result[0]['total'];
        } else {
            return 0;
        }
    }

    /**
     * Returns a list of distinct values for the given column across a collection.
338 339 340 341
     * @param string $q column to use.
     * @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 array of distinct values
342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357
     */
    public function distinct($q, $db = null)
    {
        $collection = $this->getCollection($db);
        if ($this->where !== null) {
            $condition = $this->where;
        } else {
            $condition = [];
        }
        $result = $collection->distinct($q, $condition);
        if ($result === false) {
            return [];
        } else {
            return $result;
        }
    }
358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379

    /**
     * Composes condition from raw [[where]] value.
     * @return array conditions.
     */
    private function composeCondition()
    {
        if ($this->where === null) {
            return [];
        } else {
            return $this->where;
        }
    }

    /**
     * Composes select fields from raw [[select]] value.
     * @return array select fields.
     */
    private function composeSelectFields()
    {
        $selectFields = [];
        if (!empty($this->select)) {
380 381 382 383 384 385
            foreach ($this->select as $key => $value) {
                if (is_numeric($key)) {
                    $selectFields[$value] = true;
                } else {
                    $selectFields[$key] = $value;
                }
386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402
            }
        }
        return $selectFields;
    }

    /**
     * Composes sort specification from raw [[orderBy]] value.
     * @return array sort specification.
     */
    private function composeSort()
    {
        $sort = [];
        foreach ($this->orderBy as $fieldName => $sortOrder) {
            $sort[$fieldName] = $sortOrder === SORT_DESC ? \MongoCollection::DESCENDING : \MongoCollection::ASCENDING;
        }
        return $sort;
    }
AlexGx committed
403
}