ActiveQuery.php 9.5 KB
Newer Older
Qiang Xue committed
1 2 3 4
<?php
/**
 * @author Qiang Xue <qiang.xue@gmail.com>
 * @link http://www.yiiframework.com/
Qiang Xue committed
5
 * @copyright Copyright (c) 2008 Yii Software LLC
Qiang Xue committed
6 7 8
 * @license http://www.yiiframework.com/license/
 */

Qiang Xue committed
9
namespace yii\db;
Qiang Xue committed
10

Qiang Xue committed
11 12 13 14 15 16 17 18 19 20
/**
 * ActiveQuery represents a DB query associated with an Active Record class.
 *
 * ActiveQuery instances are usually created by [[ActiveRecord::find()]], [[ActiveRecord::findBySql()]]
 * and [[ActiveRecord::count()]].
 *
 * ActiveQuery mainly provides the following methods to retrieve the query results:
 *
 * - [[one()]]: returns a single record populated with the first row of data.
 * - [[all()]]: returns all records based on the query results.
21 22 23 24 25 26
 * - [[count()]]: returns the number of records.
 * - [[sum()]]: returns the sum over the specified column.
 * - [[average()]]: returns the average over the specified column.
 * - [[min()]]: returns the min over the specified column.
 * - [[max()]]: returns the max over the specified column.
 * - [[scalar()]]: returns the value of the first column in the first row of the query result.
Qiang Xue committed
27 28 29 30 31 32 33
 * - [[exists()]]: returns a value indicating whether the query result has data or not.
 *
 * Because ActiveQuery extends from [[Query]], one can use query methods, such as [[where()]],
 * [[orderBy()]] to customize the query options.
 *
 * ActiveQuery also provides the following additional query options:
 *
Qiang Xue committed
34 35 36
 * - [[with()]]: list of relations that this query should be performed with.
 * - [[indexBy()]]: the name of the column by which the query result should be indexed.
 * - [[asArray()]]: whether to return each record as an array.
Qiang Xue committed
37 38 39 40 41 42 43 44 45 46
 *
 * These options can be configured using methods of the same name. For example:
 *
 * ~~~
 * $customers = Customer::find()->with('orders')->asArray()->all();
 * ~~~
 *
 * @author Qiang Xue <qiang.xue@gmail.com>
 * @since 2.0
 */
Qiang Xue committed
47
class ActiveQuery extends Query
Qiang Xue committed
48 49 50 51 52 53 54 55
{
	/**
	 * @var string the name of the ActiveRecord class.
	 */
	public $modelClass;
	/**
	 * @var array list of relations that this query should be performed with
	 */
Qiang Xue committed
56
	public $with;
Qiang Xue committed
57
	/**
58 59 60
	 * @var string|callable $column the name of the column by which the query results should be indexed by.
	 * This can also be a callable (e.g. anonymous function) that returns the index value based on the given
	 * row or model data. For more details, see [[indexBy()]].
Qiang Xue committed
61
	 */
Qiang Xue committed
62
	public $indexBy;
Qiang Xue committed
63 64 65 66 67 68 69 70 71 72 73 74
	/**
	 * @var boolean whether to return each record as an array. If false (default), an object
	 * of [[modelClass]] will be created to represent each record.
	 */
	public $asArray;
	/**
	 * @var string the SQL statement to be executed for retrieving AR records.
	 * This is set by [[ActiveRecord::findBySql()]].
	 */
	public $sql;


75 76 77 78 79 80 81 82 83 84 85 86
	/**
	 * PHP magic method.
	 * This method allows calling static method defined in [[modelClass]] via this query object.
	 * It is mainly implemented for supporting the feature of scope.
	 * @param string $name the method name to be called
	 * @param array $params the parameters passed to the method
	 * @return mixed the method return result
	 */
	public function __call($name, $params)
	{
		if (method_exists($this->modelClass, $name)) {
			array_unshift($params, $this);
Qiang Xue committed
87 88
			call_user_func_array(array($this->modelClass, $name), $params);
			return $this;
89 90 91 92 93
		} else {
			return parent::__call($name, $params);
		}
	}

Qiang Xue committed
94 95
	/**
	 * Executes query and returns all results as an array.
96 97
	 * @param Connection $db the DB connection used to create the DB command.
	 * If null, the DB connection returned by [[modelClass]] will be used.
Qiang Xue committed
98 99
	 * @return array the query results. If the query results in nothing, an empty array will be returned.
	 */
100
	public function all($db = null)
Qiang Xue committed
101
	{
102
		$command = $this->createCommand($db);
Qiang Xue committed
103
		$rows = $command->queryAll();
104
		if (!empty($rows)) {
105 106
			$models = $this->createModels($rows);
			if (!empty($this->with)) {
Qiang Xue committed
107
				$this->populateRelations($models, $this->with);
108 109 110
			}
			return $models;
		} else {
Qiang Xue committed
111 112
			return array();
		}
Qiang Xue committed
113 114 115 116
	}

	/**
	 * Executes query and returns a single row of result.
117 118
	 * @param Connection $db the DB connection used to create the DB command.
	 * If null, the DB connection returned by [[modelClass]] will be used.
Qiang Xue committed
119 120
	 * @return ActiveRecord|array|null a single row of query result. Depending on the setting of [[asArray]],
	 * the query result may be either an array or an ActiveRecord object. Null will be returned
Qiang Xue committed
121 122
	 * if the query results in nothing.
	 */
123
	public function one($db = null)
Qiang Xue committed
124
	{
125
		$command = $this->createCommand($db);
126
		$row = $command->queryOne();
127
		if ($row !== false && !$this->asArray) {
Qiang Xue committed
128
			/** @var $class ActiveRecord */
Qiang Xue committed
129
			$class = $this->modelClass;
Qiang Xue committed
130 131
			$model = $class::create($row);
			if (!empty($this->with)) {
Qiang Xue committed
132
				$models = array($model);
Qiang Xue committed
133
				$this->populateRelations($models, $this->with);
Qiang Xue committed
134
				$model = $models[0];
Qiang Xue committed
135 136
			}
			return $model;
137 138
		} else {
			return $row === false ? null : $row;
Qiang Xue committed
139
		}
Qiang Xue committed
140 141 142
	}

	/**
Qiang Xue committed
143
	 * Creates a DB command that can be used to execute this query.
Qiang Xue committed
144 145
	 * @param Connection $db the DB connection used to create the DB command.
	 * If null, the DB connection returned by [[modelClass]] will be used.
Qiang Xue committed
146
	 * @return Command the created DB command instance.
Qiang Xue committed
147
	 */
Qiang Xue committed
148
	public function createCommand($db = null)
Qiang Xue committed
149
	{
Qiang Xue committed
150
		/** @var $modelClass ActiveRecord */
Qiang Xue committed
151
		$modelClass = $this->modelClass;
Qiang Xue committed
152
		if ($db === null) {
Qiang Xue committed
153
			$db = $modelClass::getDb();
Qiang Xue committed
154
		}
Qiang Xue committed
155 156 157
		if ($this->sql === null) {
			if ($this->from === null) {
				$tableName = $modelClass::tableName();
Qiang Xue committed
158 159 160
				if ($this->select === null && !empty($this->join)) {
					$this->select = array("$tableName.*");
				}
Qiang Xue committed
161 162 163 164
				$this->from = array($tableName);
			}
			/** @var $qb QueryBuilder */
			$qb = $db->getQueryBuilder();
Qiang Xue committed
165
			$this->sql = $qb->build($this);
Qiang Xue committed
166
		}
Qiang Xue committed
167
		return $db->createCommand($this->sql, $this->params);
Qiang Xue committed
168 169
	}

Qiang Xue committed
170 171 172 173 174
	/**
	 * Sets the [[asArray]] property.
	 * @param boolean $value whether to return the query results in terms of arrays instead of Active Records.
	 * @return ActiveQuery the query object itself
	 */
Qiang Xue committed
175 176 177 178 179 180
	public function asArray($value = true)
	{
		$this->asArray = $value;
		return $this;
	}

Qiang Xue committed
181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202
	/**
	 * Specifies the relations with which this query should be performed.
	 *
	 * The parameters to this method can be either one or multiple strings, or a single array
	 * of relation names and the optional callbacks to customize the relations.
	 *
	 * The followings are some usage examples:
	 *
	 * ~~~
	 * // find customers together with their orders and country
	 * Customer::find()->with('orders', 'country')->all();
	 * // find customers together with their country and orders of status 1
	 * Customer::find()->with(array(
	 *     'orders' => function($query) {
	 *         $query->andWhere('status = 1');
	 *     },
	 *     'country',
	 * ))->all();
	 * ~~~
	 *
	 * @return ActiveQuery the query object itself
	 */
Qiang Xue committed
203 204 205 206 207 208 209 210 211 212
	public function with()
	{
		$this->with = func_get_args();
		if (isset($this->with[0]) && is_array($this->with[0])) {
			// the parameter is given as an array
			$this->with = $this->with[0];
		}
		return $this;
	}

Qiang Xue committed
213 214
	/**
	 * Sets the [[indexBy]] property.
215 216 217 218 219 220 221 222 223 224 225 226 227
	 * @param string|callable $column the name of the column by which the query results should be indexed by.
	 * This can also be a callable (e.g. anonymous function) that returns the index value based on the given
	 * row or model data. The signature of the callable should be:
	 *
	 * ~~~
	 * // $model is an AR instance when `asArray` is false,
	 * // or an array of column values when `asArray` is true.
	 * function ($model)
	 * {
	 *     // return the index value corresponding to $model
	 * }
	 * ~~~
	 *
Qiang Xue committed
228 229
	 * @return ActiveQuery the query object itself
	 */
Qiang Xue committed
230
	public function indexBy($column)
Qiang Xue committed
231
	{
Qiang Xue committed
232
		$this->indexBy = $column;
Qiang Xue committed
233 234 235
		return $this;
	}

Qiang Xue committed
236
	private function createModels($rows)
Qiang Xue committed
237 238 239
	{
		$models = array();
		if ($this->asArray) {
Qiang Xue committed
240
			if ($this->indexBy === null) {
Qiang Xue committed
241 242 243
				return $rows;
			}
			foreach ($rows as $row) {
244 245 246 247 248 249
				if (is_string($this->indexBy)) {
					$key = $row[$this->indexBy];
				} else {
					$key = call_user_func($this->indexBy, $row);
				}
				$models[$key] = $row;
Qiang Xue committed
250 251 252 253
			}
		} else {
			/** @var $class ActiveRecord */
			$class = $this->modelClass;
Qiang Xue committed
254
			if ($this->indexBy === null) {
Qiang Xue committed
255 256 257 258 259 260
				foreach ($rows as $row) {
					$models[] = $class::create($row);
				}
			} else {
				foreach ($rows as $row) {
					$model = $class::create($row);
261 262 263 264 265 266
					if (is_string($this->indexBy)) {
						$key = $model->{$this->indexBy};
					} else {
						$key = call_user_func($this->indexBy, $model);
					}
					$models[$key] = $model;
Qiang Xue committed
267 268 269 270 271
				}
			}
		}
		return $models;
	}
Qiang Xue committed
272

Qiang Xue committed
273
	private function populateRelations(&$models, $with)
Qiang Xue committed
274 275
	{
		$primaryModel = new $this->modelClass;
Qiang Xue committed
276 277
		$relations = $this->normalizeRelations($primaryModel, $with);
		foreach ($relations as $name => $relation) {
Qiang Xue committed
278 279 280 281
			if ($relation->asArray === null) {
				// inherit asArray from primary query
				$relation->asArray = $this->asArray;
			}
Qiang Xue committed
282
			$relation->findWith($name, $models);
Qiang Xue committed
283 284
		}
	}
Qiang Xue committed
285 286 287 288 289 290

	/**
	 * @param ActiveRecord $model
	 * @param array $with
	 * @return ActiveRelation[]
	 */
Qiang Xue committed
291
	private function normalizeRelations($model, $with)
Qiang Xue committed
292 293
	{
		$relations = array();
Qiang Xue committed
294
		foreach ($with as $name => $callback) {
Qiang Xue committed
295
			if (is_integer($name)) {
Qiang Xue committed
296 297
				$name = $callback;
				$callback = null;
Qiang Xue committed
298 299 300 301 302 303 304 305 306
			}
			if (($pos = strpos($name, '.')) !== false) {
				// with sub-relations
				$childName = substr($name, $pos + 1);
				$name = substr($name, 0, $pos);
			} else {
				$childName = null;
			}

Qiang Xue committed
307 308
			$t = strtolower($name);
			if (!isset($relations[$t])) {
309 310 311
				$relation = $model->getRelation($name);
				$relation->primaryModel = null;
				$relations[$t] = $relation;
Qiang Xue committed
312
			} else {
Qiang Xue committed
313
				$relation = $relations[$t];
Qiang Xue committed
314 315 316
			}

			if (isset($childName)) {
Qiang Xue committed
317 318 319
				$relation->with[$childName] = $callback;
			} elseif ($callback !== null) {
				call_user_func($callback, $relation);
Qiang Xue committed
320 321 322 323
			}
		}
		return $relations;
	}
Qiang Xue committed
324
}