ActiveFinder.php 16.2 KB
Newer Older
Qiang Xue committed
1 2
<?php
/**
Qiang Xue committed
3
 * ActiveFinder class file.
Qiang Xue committed
4 5 6 7 8 9 10 11 12
 *
 * @author Qiang Xue <qiang.xue@gmail.com>
 * @link http://www.yiiframework.com/
 * @copyright Copyright &copy; 2008-2012 Yii Software LLC
 * @license http://www.yiiframework.com/license/
 */

namespace yii\db\ar;

Qiang Xue committed
13
use yii\base\Object;
Qiang Xue committed
14
use yii\base\VectorIterator;
Qiang Xue committed
15
use yii\db\dao\Query;
Qiang Xue committed
16
use yii\db\Exception;
Qiang Xue committed
17

Qiang Xue committed
18 19
/**
 * ActiveFinder.php is ...
Qiang Xue committed
20
 *
Qiang Xue committed
21 22
 * @property integer $count
 *
Qiang Xue committed
23 24 25
 * @author Qiang Xue <qiang.xue@gmail.com>
 * @since 2.0
 */
Qiang Xue committed
26
class ActiveFinder extends \yii\base\Object
Qiang Xue committed
27
{
.  
Qiang Xue committed
28
	/**
Qiang Xue committed
29
	 * @var \yii\db\dao\Connection
.  
Qiang Xue committed
30
	 */
Qiang Xue committed
31
	public $connection;
Qiang Xue committed
32

Qiang Xue committed
33
	public function __construct($connection)
Qiang Xue committed
34
	{
Qiang Xue committed
35
		$this->connection = $connection;
Qiang Xue committed
36 37 38
	}

	/**
Qiang Xue committed
39
	 * @param ActiveQuery $query
Qiang Xue committed
40
	 */
Qiang Xue committed
41
	public function find($query, $returnScalar = false)
Qiang Xue committed
42
	{
Qiang Xue committed
43
		if (!empty($query->with)) {
Qiang Xue committed
44
			return $this->findWithRelations($query, $returnScalar);
Qiang Xue committed
45 46
		}

Qiang Xue committed
47 48
		if ($query->sql !== null) {
			$sql = $query->sql;
Qiang Xue committed
49
		} else {
Qiang Xue committed
50 51
			$modelClass = $query->modelClass;
			$tableName = $modelClass::tableName();
Qiang Xue committed
52 53 54 55 56 57
			if ($query->from === null) {
				if ($query->tableAlias !== null) {
					$tableName .= ' ' . $query->tableAlias;
				}
				$query->from = array($tableName);
			}
Qiang Xue committed
58 59
			$this->applyScopes($query);
			$sql = $this->connection->getQueryBuilder()->build($query);
Qiang Xue committed
60 61 62 63 64

			if ($query->tableAlias !== null) {
				$alias = $this->connection->quoteTableName($query->tableAlias) . '.';
			} else {
				$alias = $this->connection->quoteTableName($tableName) . '.';
Qiang Xue committed
65
			}
Qiang Xue committed
66 67 68 69 70
			$tokens = array(
				'@.' => $alias,
				$this->connection->quoteTableName('@', true) . '.' => $alias,
			);
			$sql = strtr($sql, $tokens);
Qiang Xue committed
71
		}
Qiang Xue committed
72
		$command = $this->connection->createCommand($sql, $query->params);
Qiang Xue committed
73

Qiang Xue committed
74 75 76 77 78 79
		if ($returnScalar) {
			return $command->queryScalar();
		} else {
			$rows = $command->queryAll();
			return $this->createRecords($query, $rows);
		}
80 81
	}

Qiang Xue committed
82 83 84 85 86 87 88 89 90
	private $_joinCount;
	private $_tableAliases;
	private $_hasMany;

	/**
	 * @param ActiveQuery $query
	 * @return array
	 */
	protected function findWithRelations($query, $returnScalar = false)
91
	{
Qiang Xue committed
92 93 94 95 96 97 98 99 100
		$this->_joinCount = 0;
		$this->_tableAliases = array();
		$this->_hasMany = false;
		$joinTree = new JoinElement($this->_joinCount++, $query, null, null);

		if ($query->sql !== null) {
			$command = $this->connection->createCommand($query->sql, $query->params);
			if ($returnScalar) {
				return $command->queryScalar();
Qiang Xue committed
101
			}
Qiang Xue committed
102 103 104 105 106 107 108 109 110 111 112
			$rows = $command->queryAll();
			$records = $this->createRecords($query, $rows);
			$modelClass = $query->modelClass;
			$table = $modelClass::getMetaData()->table;
			foreach ($records as $record) {
				$pk = array();
				foreach ($table->primaryKey as $name) {
					$pk[] = $record[$name];
				}
				$pk = count($pk) === 1 ? $pk[0] : serialize($pk);
				$joinTree->records[$pk] = $record;
Qiang Xue committed
113
			}
Qiang Xue committed
114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132

			$q = new ActiveQuery($modelClass);
			$q->with = $query->with;
			$q->tableAlias = 't';
			$q->asArray = $query->asArray;
			$q->index = $query->index;
			$q->select = $table->primaryKey;
			$this->addPkCondition($q, $table, $rows, 't.');
			$joinTree->query = $query = $q;
		}

		$this->buildJoinTree($joinTree, $query->with);
		$this->initJoinTree($joinTree);

		$q = new Query;
		$this->buildJoinQuery($joinTree, $q, $returnScalar);

		if ($returnScalar) {
			return $q->createCommand($this->connection)->queryScalar();
Qiang Xue committed
133
		} else {
Qiang Xue committed
134 135
			if ($this->_hasMany && ($query->limit > 0 || $query->offset > 0)) {
				$this->limitQuery($query, $q);
Qiang Xue committed
136
			}
Qiang Xue committed
137 138 139 140
			$command = $q->createCommand($this->connection);
			$rows = $command->queryAll();
			$joinTree->populateData($rows);
			return $query->index === null ? array_values($joinTree->records) : $joinTree->records;
Qiang Xue committed
141 142 143
		}
	}

Qiang Xue committed
144 145 146 147 148
	/**
	 * @param ActiveRecord $record
	 * @param ActiveRelation $relation
	 * @return array
	 */
Qiang Xue committed
149
	public function findWithRecord($record, $relation)
Qiang Xue committed
150
	{
Qiang Xue committed
151 152 153 154
		$this->_joinCount = 0;
		$this->_tableAliases = array();
		$this->_hasMany = false;
		$query = new ActiveQuery(get_class($record));
Qiang Xue committed
155 156 157 158 159
		$modelClass = $query->modelClass;
		$table = $modelClass::getMetaData()->table;
		$query->select = $table->primaryKey;
		$query->limit = $relation->limit;
		$query->offset = $relation->offset;
Qiang Xue committed
160 161
		$joinTree = new JoinElement($this->_joinCount++, $query, null, null);
		$child = $this->buildJoinTree($joinTree, $relation->name);
Qiang Xue committed
162 163
		$child->query = $relation;
		$child->container = null;
Qiang Xue committed
164 165
		$this->buildJoinTree($child, $relation->with);
		$this->initJoinTree($joinTree);
Qiang Xue committed
166

Qiang Xue committed
167 168
		$pk = $record->getPrimaryKey(true);
		$this->addPkCondition($query, $table, array($pk), $query->tableAlias . '.');
Qiang Xue committed
169 170 171 172 173 174 175 176 177

		$q = new Query;
		$this->buildJoinQuery($joinTree, $q);

		if ($this->_hasMany && ($query->limit > 0 || $query->offset > 0)) {
			$this->limitQuery($query, $q);
		}

		$rows = $q->createCommand($this->connection)->queryAll();
Qiang Xue committed
178
		$child->populateData($rows);
Qiang Xue committed
179

Qiang Xue committed
180 181
		$records = $relation->index === null ? array_values($child->records) : $child->records;
		if ($relation->hasMany) {
Qiang Xue committed
182 183
			return $records;
		} else {
Qiang Xue committed
184
			return $records === array() ? null : reset($records);
Qiang Xue committed
185
		}
Qiang Xue committed
186 187
	}

Qiang Xue committed
188
	protected function createRecords($query, $rows)
Qiang Xue committed
189
	{
Qiang Xue committed
190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206
		$records = array();
		if ($query->asArray) {
			if ($query->index === null) {
				return $rows;
			}
			foreach ($rows as $row) {
				$records[$row[$query->index]] = $row;
			}
		} else {
			$class = $query->modelClass;
			if ($query->index === null) {
				foreach ($rows as $row) {
					$records[] = $class::create($row);
				}
			} else {
				foreach ($rows as $row) {
					$records[$row[$query->index]] = $class::create($row);
207 208 209
				}
			}
		}
Qiang Xue committed
210
		return $records;
Qiang Xue committed
211 212
	}

Qiang Xue committed
213 214
	protected function applyScopes($query)
	{
Qiang Xue committed
215 216
		$class = $query->modelClass;
		$class::defaultScope($query);
Qiang Xue committed
217
		if (is_array($query->scopes)) {
Qiang Xue committed
218 219 220 221
			$scopes = $class::scopes();
			foreach ($query->scopes as $name => $params) {
				if (is_integer($name)) {
					$name = $params;
Qiang Xue committed
222 223
					$params = array();
				}
Qiang Xue committed
224 225 226 227
				if (isset($scopes[$name])) {
					array_unshift($params, $query);
					call_user_func_array($scopes[$name], $params);
				} else {
Qiang Xue committed
228 229
					throw new Exception("$class has no scope named '$name'.");
				}
Qiang Xue committed
230 231 232 233
			}
		}
	}

Qiang Xue committed
234 235 236 237 238 239 240
	/**
	 * @param JoinElement $parent
	 * @param array|string $with
	 * @param array $config
	 * @return null|JoinElement
	 * @throws \yii\db\Exception
	 */
Qiang Xue committed
241
	protected function buildJoinTree($parent, $with, $config = array())
Qiang Xue committed
242
	{
Qiang Xue committed
243 244 245
		if (empty($with)) {
			return null;
		}
Qiang Xue committed
246 247
		if (is_array($with)) {
			foreach ($with as $name => $value) {
Qiang Xue committed
248
				if (is_array($value)) {
Qiang Xue committed
249
					$this->buildJoinTree($parent, $name, $value);
Qiang Xue committed
250 251
				} else {
					$this->buildJoinTree($parent, $value);
Qiang Xue committed
252 253 254 255 256 257
				}
			}
			return null;
		}

		if (($pos = strrpos($with, '.')) !== false) {
Qiang Xue committed
258
			$parent = $this->buildJoinTree($parent, substr($with, 0, $pos));
Qiang Xue committed
259 260 261 262 263 264
			$with = substr($with, $pos + 1);
		}

		if (isset($parent->children[$with])) {
			$child = $parent->children[$with];
		} else {
Qiang Xue committed
265
			$modelClass = $parent->query->modelClass;
Qiang Xue committed
266 267 268 269 270
			$relations = $modelClass::getMetaData()->relations;
			if (!isset($relations[$with])) {
				throw new Exception("$modelClass has no relation named '$with'.");
			}
			$relation = clone $relations[$with];
Qiang Xue committed
271
			if (is_string($relation->via)) {
272
				// join via an existing relation
Qiang Xue committed
273
				$parent2 = $this->buildJoinTree($parent, $relation->via);
Qiang Xue committed
274 275 276
				if ($parent2->query->select === null) {
					$parent2->query->select = false;
					unset($parent2->container->relations[$parent2->query->name]);
Qiang Xue committed
277
				}
Qiang Xue committed
278
				$child = new JoinElement($this->_joinCount++, $relation, $parent2, $parent);
Qiang Xue committed
279 280 281
			} elseif (is_array($relation->via)) {
				// join via a pivoting table
				$r = new ActiveRelation;
Qiang Xue committed
282
				$r->name = 'vt' . $this->_joinCount;
Qiang Xue committed
283 284 285 286 287 288 289 290 291 292 293 294 295 296
				$r->hasMany = $relation->hasMany;

				foreach ($relation->via as $name => $value) {
					$r->$name = $value;
				}

				$r->select = false;
				if ($r->joinType === null) {
					$r->joinType = $relation->joinType;
				}

				$parent2 = new JoinElement($this->_joinCount++, $r, $parent, $parent);
				$child = new JoinElement($this->_joinCount++, $relation, $parent2, $parent);

Qiang Xue committed
297
			} else {
Qiang Xue committed
298
				$child = new JoinElement($this->_joinCount++, $relation, $parent, $parent);
Qiang Xue committed
299 300 301 302
			}
		}

		foreach ($config as $name => $value) {
Qiang Xue committed
303
			$child->query->$name = $value;
Qiang Xue committed
304 305 306 307
		}

		return $child;
	}
Qiang Xue committed
308

Qiang Xue committed
309 310 311
	/**
	 * @param JoinElement $element
	 */
Qiang Xue committed
312
	protected function initJoinTree($element)
Qiang Xue committed
313
	{
Qiang Xue committed
314 315 316 317 318 319
		if ($element->query->tableAlias !== null) {
			$alias = $element->query->tableAlias;
		} elseif ($element->query instanceof ActiveRelation) {
			$alias = $element->query->name;
		} else {
			$alias = 't';
Qiang Xue committed
320
		}
321 322 323 324 325 326 327 328
		if ($element->query instanceof ActiveRelation) {
			if ($element->query->hasMany) {
				$this->_hasMany = true;
			}
			if ($element->parent->query->asArray !== null && $element->query->asArray === null) {
				$element->query->asArray = $element->parent->query->asArray;
			}
		}
Qiang Xue committed
329 330 331 332 333 334 335
		$count = 0;
		while (isset($this->_tableAliases[$alias])) {
			$alias = 't' . $count++;
		}
		$this->_tableAliases[$alias] = true;
		$element->query->tableAlias = $alias;

Qiang Xue committed
336
		if ($element->records !== array()) {
Qiang Xue committed
337 338 339 340 341 342
			$this->applyScopes($element->query);
		}

		if ($element->container !== null && $element->query->asArray === null) {
			$element->query->asArray = $element->container->query->asArray;
		}
Qiang Xue committed
343

Qiang Xue committed
344
		foreach ($element->children as $child) {
Qiang Xue committed
345
			$this->initJoinTree($child);
Qiang Xue committed
346 347
		}
	}
Qiang Xue committed
348 349 350

	/**
	 * @param JoinElement $element
Qiang Xue committed
351
	 * @param \yii\db\dao\Query $query
Qiang Xue committed
352
	 */
Qiang Xue committed
353
	protected function buildJoinQuery($element, $query, $keepSelect = false)
Qiang Xue committed
354
	{
Qiang Xue committed
355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370
		if ($element->parent) {
			$prefixes = array(
				'@.' => $element->query->tableAlias . '.',
				'?.' => $element->parent->query->tableAlias . '.',
			);
			$quotedPrefixes = array(
				'@.' => $this->connection->quoteTableName($element->query->tableAlias, true) . '.',
				'?.' => $this->connection->quoteTableName($element->parent->query->tableAlias, true) . '.',
			);
		} else {
			$prefixes = array(
				'@.' => $element->query->tableAlias . '.',
			);
			$quotedPrefixes = array(
				'@.' => $this->connection->quoteTableName($element->query->tableAlias, true) . '.',
			);
Qiang Xue committed
371 372
			$query->limit = $element->query->limit;
			$query->offset = $element->query->offset;
Qiang Xue committed
373 374 375
		}

		$qb = $this->connection->getQueryBuilder();
Qiang Xue committed
376

Qiang Xue committed
377 378 379 380 381 382 383 384 385 386 387 388 389 390
		if ($keepSelect) {
			if (!empty($element->query->select)) {
				$select = $element->query->select;
				if (is_string($select)) {
					$select = explode(',', $select);
				}
				foreach ($select as $column) {
					$query->select[] = strtr(trim($column), $prefixes);
				}
			}
		} else {
			foreach ($this->buildSelect($element, $element->query->select) as $column) {
				$query->select[] = strtr($column, $prefixes);
			}
Qiang Xue committed
391 392
		}

Qiang Xue committed
393 394 395 396 397 398 399 400 401 402 403
		if ($element->query instanceof ActiveQuery) {
			if ($element->query->from === null) {
				$modelClass = $element->query->modelClass;
				$tableName = $modelClass::tableName();
				if ($element->query->tableAlias !== null) {
					$tableName .= ' ' . $element->query->tableAlias;
				}
				$query->from = array($tableName);
			} else {
				$query->from = $element->query->from;
			}
Qiang Xue committed
404 405
		}

Qiang Xue committed
406 407
		if (($where = $qb->buildCondition($element->query->where)) !== '') {
			$query->andWhere(strtr($where, $quotedPrefixes));
Qiang Xue committed
408 409
		}

Qiang Xue committed
410 411
		if (($having = $qb->buildCondition($element->query->having)) !== '') {
			$query->andHaving(strtr($having, $quotedPrefixes));
Qiang Xue committed
412 413
		}

Qiang Xue committed
414
		if ($element->query instanceof ActiveRelation) {
Qiang Xue committed
415
			$joinType = $element->query->joinType ?: 'LEFT JOIN';
Qiang Xue committed
416 417 418 419 420 421
			if ($element->query->modelClass !== null) {
				$modelClass = $element->query->modelClass;
				$tableName = $this->connection->quoteTableName($modelClass::tableName());
			} else {
				$tableName = $this->connection->quoteTableName($element->query->table);
			}
Qiang Xue committed
422 423
			$tableAlias = $this->connection->quoteTableName($element->query->tableAlias);
			$join = "$joinType $tableName $tableAlias";
Qiang Xue committed
424 425 426 427 428 429 430 431 432 433 434
			$on = '';
			if (is_array($element->query->link)) {
				foreach ($element->query->link as $pk => $fk) {
					$pk = $quotedPrefixes['@.'] . $this->connection->quoteColumnName($pk, true);
					$fk = $quotedPrefixes['?.'] . $this->connection->quoteColumnName($fk, true);
					if ($on !== '') {
						$on .= ' AND ';
					}
					$on .= "$pk = $fk";
				}
			}
Qiang Xue committed
435
			if ($element->query->on !== null) {
Qiang Xue committed
436 437 438 439 440 441 442 443 444
				$condition = strtr($qb->buildCondition($element->query->on), $quotedPrefixes);
				if ($on !== '') {
					$on .= " AND ($condition)";
				} else {
					$on = $condition;
				}
			}
			if ($on !== '') {
				$join .= ' ON ' . $on;
Qiang Xue committed
445 446
			}
			$query->join[] = $join;
Qiang Xue committed
447 448 449
		}

		if ($element->query->join !== null) {
Qiang Xue committed
450 451 452 453 454 455 456 457 458 459
			if (is_array($element->query->join)) {
				foreach ($element->query->join as $join) {
					if (is_array($join) && isset($join[2])) {
						$join[2] = strtr($join[2], $quotedPrefixes);
					}
					$query->join[] = $join;
				}
			} else {
				$query->join[] = strtr($element->query->join, $quotedPrefixes);
			}
Qiang Xue committed
460 461
		}

462 463 464
		if ($element->query->order !== null) {
			if (!is_array($element->query->order)) {
				$element->query->order = preg_split('/\s*,\s*/', trim($element->query->order), -1, PREG_SPLIT_NO_EMPTY);
Qiang Xue committed
465
			}
466 467
			foreach ($element->query->order as $order) {
				$query->order[] = strtr($order, $prefixes);
Qiang Xue committed
468 469 470
			}
		}

471 472 473
		if ($element->query->group !== null) {
			if (!is_array($element->query->group)) {
				$element->query->group = preg_split('/\s*,\s*/', trim($element->query->group), -1, PREG_SPLIT_NO_EMPTY);
Qiang Xue committed
474
			}
475 476
			foreach ($element->query->group as $group) {
				$query->group[] = strtr($group, $prefixes);
Qiang Xue committed
477 478 479 480 481 482 483 484
			}
		}

		if ($element->query->params !== null) {
			$query->addParams($element->query->params);
		}

		foreach ($element->children as $child) {
Qiang Xue committed
485
			$this->buildJoinQuery($child, $query, $keepSelect);
Qiang Xue committed
486 487 488 489 490 491 492 493 494 495 496 497 498
		}
	}

	protected function buildSelect($element, $select)
	{
		if ($select === false) {
			return array();
		}
		$modelClass = $element->query->modelClass;
		$table = $modelClass::getMetaData()->table;
		$columns = array();
		$columnCount = 0;
		$prefix = $element->query->tableAlias;
Qiang Xue committed
499 500 501 502 503 504 505 506

		foreach ($table->primaryKey as $column) {
			$alias = "c{$element->id}_" . ($columnCount++);
			$columns[] = "$prefix.$column AS $alias";
			$element->pkAlias[$column] = $alias;
			$element->columnAliases[$alias] = $column;
		}

Qiang Xue committed
507 508
		if (empty($select) || $select === '*') {
			foreach ($table->columns as $column) {
Qiang Xue committed
509 510 511 512
				if (!isset($element->pkAlias[$column->name])) {
					$alias = "c{$element->id}_" . ($columnCount++);
					$columns[] = "$prefix.{$column->name} AS $alias";
					$element->columnAliases[$alias] = $column->name;
Qiang Xue committed
513 514 515 516 517 518 519 520 521 522 523 524 525
				}
			}
		} else {
			if (is_string($select)) {
				$select = explode(',', $select);
			}
			foreach ($select as $column) {
				$column = trim($column);
				if (preg_match('/^(.*?)\s+AS\s+(\w+)$/im', $column, $matches)) {
					// if the column is already aliased
					$element->columnAliases[$matches[2]] = $matches[2];
					$columns[] = $column;
				} elseif (!isset($element->pkAlias[$column])) {
Qiang Xue committed
526
					$alias = "c{$element->id}_" . ($columnCount++);
Qiang Xue committed
527 528 529 530 531
					if (strpos($column, '(') !== false) {
						$columns[] = "$column AS $alias";
					} else {
						$columns[] = "$prefix.$column AS $alias";
					}
Qiang Xue committed
532 533 534 535 536
					$element->columnAliases[$alias] = $column;
				}
			}
		}

Qiang Xue committed
537 538 539 540 541 542 543 544 545 546 547 548
		// determine the actual index column(s)
		if ($element->query->index !== null) {
			$index = array_search($element->query->index, $element->columnAliases);
		}
		if (empty($index)) {
			$index = $element->pkAlias;
			if (count($index) === 1) {
				$index = reset($element->pkAlias);
			}
		}
		$element->key = $index;

Qiang Xue committed
549 550
		return $columns;
	}
Qiang Xue committed
551 552 553 554 555 556 557 558 559 560 561 562

	protected function limitQuery($activeQuery, $query)
	{
		$q = clone $query;
		$modelClass = $activeQuery->modelClass;
		$table = $modelClass::getMetaData()->table;
		$q->select = array();
		foreach ($table->primaryKey as $name) {
			$q->select[] = $alias = $activeQuery->tableAlias . '.' . $name;
		}
		$q->distinct = true;
		$rows = $q->createCommand($this->connection)->queryAll();
563 564 565 566 567 568 569
		$prefix = $activeQuery->tableAlias . '.';
		$this->addPkCondition($query, $table, $rows, $prefix);
		$query->limit = $query->offset = null;
	}

	protected function addPkCondition($query, $table, $rows, $prefix)
	{
Qiang Xue committed
570
		if (count($table->primaryKey) === 1 && count($rows) > 1) {
Qiang Xue committed
571 572 573 574 575
			$name = $table->primaryKey[0];
			$values = array();
			foreach ($rows as $row) {
				$values[] = $table->columns[$name]->typecast($row[$name]);
			}
576
			$query->andWhere(array('in', $prefix . $name, $values));
Qiang Xue committed
577 578 579
		} else {
			$ors = array('or');
			foreach ($rows as $row) {
580
				$hash = array();
Qiang Xue committed
581 582 583 584 585
				foreach ($table->primaryKey as $name) {
					$value = $table->columns[$name]->typecast($row[$name]);
					if (is_string($value)) {
						$value = $this->connection->quoteValue($value);
					}
586
					$hash[$prefix . $name] = $value;
Qiang Xue committed
587
				}
588
				$ors[] = $hash;
Qiang Xue committed
589 590 591 592
			}
			$query->andWhere($ors);
		}
	}
Qiang Xue committed
593
}