Yii2 framework backup
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.
 
 
 
 
 

717 lines
26 KiB

<?php
/**
* @author Qiang Xue <qiang.xue@gmail.com>
* @link http://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license http://www.yiiframework.com/license/
*/
namespace yii\db;
/**
* ActiveQuery represents a DB query associated with an Active Record class.
*
* An ActiveQuery can be a normal query or be used in a relational context.
*
* ActiveQuery instances are usually created by [[ActiveRecord::find()]] and [[ActiveRecord::findBySql()]].
* Relational queries are created by [[ActiveRecord::hasOne()]] and [[ActiveRecord::hasMany()]].
*
* Normal Query
* ------------
*
* 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.
* - [[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.
* - [[column()]]: returns the value of the first column in the query result.
* - [[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:
*
* - [[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.
*
* These options can be configured using methods of the same name. For example:
*
* ```php
* $customers = Customer::find()->with('orders')->asArray()->all();
* ```
*
* Relational query
* ----------------
*
* In relational context ActiveQuery represents a relation between two Active Record classes.
*
* Relational ActiveQuery instances are usually created by calling [[ActiveRecord::hasOne()]] and
* [[ActiveRecord::hasMany()]]. An Active Record class declares a relation by defining
* a getter method which calls one of the above methods and returns the created ActiveQuery object.
*
* A relation is specified by [[link]] which represents the association between columns
* of different tables; and the multiplicity of the relation is indicated by [[multiple]].
*
* If a relation involves a pivot table, it may be specified by [[via()]] or [[viaTable()]] method.
* These methods may only be called in a relational context. Same is true for [[inverseOf()]], which
* marks a relation as inverse of another relation and [[onCondition()]] which adds a condition that
* is to be added to relational query join condition.
*
* @author Qiang Xue <qiang.xue@gmail.com>
* @author Carsten Brandt <mail@cebe.cc>
* @since 2.0
*/
class ActiveQuery extends Query implements ActiveQueryInterface
{
use ActiveQueryTrait;
use ActiveRelationTrait;
/**
* @var string the SQL statement to be executed for retrieving AR records.
* This is set by [[ActiveRecord::findBySql()]].
*/
public $sql;
/**
* @var string|array the join condition to be used when this query is used in a relational context.
* The condition will be used in the ON part when [[ActiveQuery::joinWith()]] is called.
* Otherwise, the condition will be used in the WHERE part of a query.
* Please refer to [[Query::where()]] on how to specify this parameter.
* @see onCondition()
*/
public $on;
/**
* @var array a list of relations that this query should be joined with
*/
public $joinWith;
/**
* Constructor.
* @param array $modelClass the model class associated with this query
* @param array $config configurations to be applied to the newly created query object
*/
public function __construct($modelClass, $config = [])
{
$this->modelClass = $modelClass;
parent::__construct($config);
}
/**
* Executes query and returns all results as an array.
* @param Connection $db the DB connection used to create the DB command.
* If null, the DB connection returned by [[modelClass]] will be used.
* @return array|ActiveRecord[] the query results. If the query results in nothing, an empty array will be returned.
*/
public function all($db = null)
{
return parent::all($db);
}
/**
* @inheritdoc
*/
public function prepareBuild($builder)
{
if (!empty($this->joinWith)) {
$this->buildJoinWith();
$this->joinWith = null; // clean it up to avoid issue https://github.com/yiisoft/yii2/issues/2687
}
if (empty($this->from)) {
/** @var ActiveRecord $modelClass */
$modelClass = $this->modelClass;
$tableName = $modelClass::tableName();
$this->from = [$tableName];
}
if (empty($this->select) && !empty($this->join)) {
foreach ((array)$this->from as $alias => $table) {
if (is_string($alias)) {
$this->select = ["$alias.*"];
} elseif (is_string($table)) {
if (preg_match('/^(.*?)\s+({{\w+}}|\w+)$/', $table, $matches)) {
$alias = $matches[2];
} else {
$alias = $table;
}
$this->select = ["$alias.*"];
}
break;
}
}
}
/**
* @inheritdoc
*/
public function prepareResult($rows)
{
if (empty($rows)) {
return [];
}
$models = $this->createModels($rows);
if (!empty($this->join) && $this->indexBy === null) {
$models = $this->removeDuplicatedModels($models);
}
if (!empty($this->with)) {
$this->findWith($this->with, $models);
}
if (!$this->asArray) {
foreach ($models as $model) {
$model->afterFind();
}
}
return $models;
}
/**
* Removes duplicated models by checking their primary key values.
* This method is mainly called when a join query is performed, which may cause duplicated rows being returned.
* @param array $models the models to be checked
* @return array the distinctive models
*/
private function removeDuplicatedModels($models)
{
$hash = [];
/** @var ActiveRecord $class */
$class = $this->modelClass;
$pks = $class::primaryKey();
if (count($pks) > 1) {
foreach ($models as $i => $model) {
$key = [];
foreach ($pks as $pk) {
$key[] = $model[$pk];
}
$key = serialize($key);
if (isset($hash[$key])) {
unset($models[$i]);
} else {
$hash[$key] = true;
}
}
} else {
$pk = reset($pks);
foreach ($models as $i => $model) {
$key = $model[$pk];
if (isset($hash[$key])) {
unset($models[$i]);
} elseif ($key !== null) {
$hash[$key] = true;
}
}
}
return array_values($models);
}
/**
* Executes query and returns a single row of result.
* @param Connection $db the DB connection used to create the DB command.
* If null, the DB connection returned by [[modelClass]] will be used.
* @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
* if the query results in nothing.
*/
public function one($db = null)
{
$command = $this->createCommand($db);
$row = $command->queryOne();
if ($row !== false) {
if ($this->asArray) {
$model = $row;
} else {
/** @var ActiveRecord $class */
$class = $this->modelClass;
$model = $class::instantiate($row);
$class::populateRecord($model, $row);
}
if (!empty($this->with)) {
$models = [$model];
$this->findWith($this->with, $models);
$model = $models[0];
}
if (!$this->asArray) {
$model->afterFind();
}
return $model;
} else {
return null;
}
}
/**
* Creates a DB command that can be used to execute this query.
* @param Connection $db the DB connection used to create the DB command.
* If null, the DB connection returned by [[modelClass]] will be used.
* @return Command the created DB command instance.
*/
public function createCommand($db = null)
{
if ($this->primaryModel === null) {
// not a relational context or eager loading
if (!empty($this->on)) {
$where = $this->where;
$this->andWhere($this->on);
$command = $this->createCommandInternal($db);
$this->where = $where;
return $command;
} else {
return $this->createCommandInternal($db);
}
} else {
// lazy loading of a relation
return $this->createRelationalCommand($db);
}
}
/**
* Creates a DB command that can be used to execute this query.
* @param Connection|null $db the DB connection used to create the DB command.
* If null, the DB connection returned by [[modelClass]] will be used.
* @return Command the created DB command instance.
*/
protected function createCommandInternal($db)
{
/** @var ActiveRecord $modelClass */
$modelClass = $this->modelClass;
if ($db === null) {
$db = $modelClass::getDb();
}
if ($this->sql === null) {
list ($sql, $params) = $db->getQueryBuilder()->build($this);
} else {
$sql = $this->sql;
$params = $this->params;
}
return $db->createCommand($sql, $params);
}
/**
* Creates a command for lazy loading of a relation.
* @param Connection|null $db the DB connection used to create the DB command.
* @return Command the created DB command instance.
*/
private function createRelationalCommand($db = null)
{
$where = $this->where;
if ($this->via instanceof self) {
// via pivot table
$viaModels = $this->via->findPivotRows([$this->primaryModel]);
$this->filterByModels($viaModels);
} elseif (is_array($this->via)) {
// via relation
/** @var ActiveQuery $viaQuery */
list($viaName, $viaQuery) = $this->via;
if ($viaQuery->multiple) {
$viaModels = $viaQuery->all();
$this->primaryModel->populateRelation($viaName, $viaModels);
} else {
$model = $viaQuery->one();
$this->primaryModel->populateRelation($viaName, $model);
$viaModels = $model === null ? [] : [$model];
}
$this->filterByModels($viaModels);
} else {
$this->filterByModels([$this->primaryModel]);
}
if (!empty($this->on)) {
$this->andWhere($this->on);
}
$command = $this->createCommandInternal($db);
$this->where = $where;
return $command;
}
/**
* Joins with the specified relations.
*
* This method allows you to reuse existing relation definitions to perform JOIN queries.
* Based on the definition of the specified relation(s), the method will append one or multiple
* JOIN statements to the current query.
*
* If the `$eagerLoading` parameter is true, the method will also eager loading the specified relations,
* which is equivalent to calling [[with()]] using the specified relations.
*
* Note that because a JOIN query will be performed, you are responsible to disambiguate column names.
*
* This method differs from [[with()]] in that it will build up and execute a JOIN SQL statement
* for the primary table. And when `$eagerLoading` is true, it will call [[with()]] in addition with the specified relations.
*
* @param array $with the relations to be joined. Each array element represents a single relation.
* The array keys are relation names, and the array values are the corresponding anonymous functions that
* can be used to modify the relation queries on-the-fly. If a relation query does not need modification,
* you may use the relation name as the array value. Sub-relations can also be specified (see [[with()]]).
* For example,
*
* ```php
* // find all orders that contain books, and eager loading "books"
* Order::find()->joinWith('books', true, 'INNER JOIN')->all();
* // find all orders, eager loading "books", and sort the orders and books by the book names.
* Order::find()->joinWith([
* 'books' => function ($query) {
* $query->orderBy('item.name');
* }
* ])->all();
* ```
*
* @param boolean|array $eagerLoading whether to eager load the relations specified in `$with`.
* When this is a boolean, it applies to all relations specified in `$with`. Use an array
* to explicitly list which relations in `$with` need to be eagerly loaded.
* @param string|array $joinType the join type of the relations specified in `$with`.
* When this is a string, it applies to all relations specified in `$with`. Use an array
* in the format of `relationName => joinType` to specify different join types for different relations.
* @return static the query object itself
*/
public function joinWith($with, $eagerLoading = true, $joinType = 'LEFT JOIN')
{
$this->joinWith[] = [(array) $with, $eagerLoading, $joinType];
return $this;
}
private function buildJoinWith()
{
$join = $this->join;
$this->join = [];
foreach ($this->joinWith as $config) {
list ($with, $eagerLoading, $joinType) = $config;
$this->joinWithRelations(new $this->modelClass, $with, $joinType);
if (is_array($eagerLoading)) {
foreach ($with as $name => $callback) {
if (is_integer($name)) {
if (!in_array($callback, $eagerLoading, true)) {
unset($with[$name]);
}
} elseif (!in_array($name, $eagerLoading, true)) {
unset($with[$name]);
}
}
} elseif (!$eagerLoading) {
$with = [];
}
$this->with($with);
}
// remove duplicated joins added by joinWithRelations that may be added
// e.g. when joining a relation and a via relation at the same time
$uniqueJoins = [];
foreach($this->join as $j) {
$uniqueJoins[serialize($j)] = $j;
}
$this->join = array_values($uniqueJoins);
if (!empty($join)) {
// append explicit join to joinWith()
// https://github.com/yiisoft/yii2/issues/2880
$this->join = empty($this->join) ? $join : array_merge($this->join, $join);
}
}
/**
* Inner joins with the specified relations.
* This is a shortcut method to [[joinWith()]] with the join type set as "INNER JOIN".
* Please refer to [[joinWith()]] for detailed usage of this method.
* @param array $with the relations to be joined with
* @param boolean|array $eagerLoading whether to eager loading the relations
* @return static the query object itself
* @see joinWith()
*/
public function innerJoinWith($with, $eagerLoading = true)
{
return $this->joinWith($with, $eagerLoading, 'INNER JOIN');
}
/**
* Modifies the current query by adding join fragments based on the given relations.
* @param ActiveRecord $model the primary model
* @param array $with the relations to be joined
* @param string|array $joinType the join type
*/
private function joinWithRelations($model, $with, $joinType)
{
$relations = [];
foreach ($with as $name => $callback) {
if (is_integer($name)) {
$name = $callback;
$callback = null;
}
$primaryModel = $model;
$parent = $this;
$prefix = '';
while (($pos = strpos($name, '.')) !== false) {
$childName = substr($name, $pos + 1);
$name = substr($name, 0, $pos);
$fullName = $prefix === '' ? $name : "$prefix.$name";
if (!isset($relations[$fullName])) {
$relations[$fullName] = $relation = $primaryModel->getRelation($name);
$this->joinWithRelation($parent, $relation, $this->getJoinType($joinType, $fullName));
} else {
$relation = $relations[$fullName];
}
$primaryModel = new $relation->modelClass;
$parent = $relation;
$prefix = $fullName;
$name = $childName;
}
$fullName = $prefix === '' ? $name : "$prefix.$name";
if (!isset($relations[$fullName])) {
$relations[$fullName] = $relation = $primaryModel->getRelation($name);
if ($callback !== null) {
call_user_func($callback, $relation);
}
$this->joinWithRelation($parent, $relation, $this->getJoinType($joinType, $fullName));
}
}
}
/**
* Returns the join type based on the given join type parameter and the relation name.
* @param string|array $joinType the given join type(s)
* @param string $name relation name
* @return string the real join type
*/
private function getJoinType($joinType, $name)
{
if (is_array($joinType) && isset($joinType[$name])) {
return $joinType[$name];
} else {
return is_string($joinType) ? $joinType : 'INNER JOIN';
}
}
/**
* Returns the table name and the table alias for [[modelClass]].
* @param ActiveQuery $query
* @return array the table name and the table alias.
*/
private function getQueryTableName($query)
{
if (empty($query->from)) {
/** @var ActiveRecord $modelClass */
$modelClass = $query->modelClass;
$tableName = $modelClass::tableName();
} else {
$tableName = '';
foreach ($query->from as $alias => $tableName) {
if (is_string($alias)) {
return [$tableName, $alias];
} else {
break;
}
}
}
if (preg_match('/^(.*?)\s+({{\w+}}|\w+)$/', $tableName, $matches)) {
$alias = $matches[2];
} else {
$alias = $tableName;
}
return [$tableName, $alias];
}
/**
* Joins a parent query with a child query.
* The current query object will be modified accordingly.
* @param ActiveQuery $parent
* @param ActiveQuery $child
* @param string $joinType
*/
private function joinWithRelation($parent, $child, $joinType)
{
$via = $child->via;
$child->via = null;
if ($via instanceof ActiveQuery) {
// via table
$this->joinWithRelation($parent, $via, $joinType);
$this->joinWithRelation($via, $child, $joinType);
return;
} elseif (is_array($via)) {
// via relation
$this->joinWithRelation($parent, $via[1], $joinType);
$this->joinWithRelation($via[1], $child, $joinType);
return;
}
list ($parentTable, $parentAlias) = $this->getQueryTableName($parent);
list ($childTable, $childAlias) = $this->getQueryTableName($child);
if (!empty($child->link)) {
if (strpos($parentAlias, '{{') === false) {
$parentAlias = '{{' . $parentAlias . '}}';
}
if (strpos($childAlias, '{{') === false) {
$childAlias = '{{' . $childAlias . '}}';
}
$on = [];
foreach ($child->link as $childColumn => $parentColumn) {
$on[] = "$parentAlias.[[$parentColumn]] = $childAlias.[[$childColumn]]";
}
$on = implode(' AND ', $on);
if (!empty($child->on)) {
$on = ['and', $on, $child->on];
}
} else {
$on = $child->on;
}
$this->join($joinType, empty($child->from) ? $childTable : $child->from, $on);
if (!empty($child->where)) {
$this->andWhere($child->where);
}
if (!empty($child->having)) {
$this->andHaving($child->having);
}
if (!empty($child->orderBy)) {
$this->addOrderBy($child->orderBy);
}
if (!empty($child->groupBy)) {
$this->addGroupBy($child->groupBy);
}
if (!empty($child->params)) {
$this->addParams($child->params);
}
if (!empty($child->join)) {
foreach ($child->join as $join) {
$this->join[] = $join;
}
}
if (!empty($child->union)) {
foreach ($child->union as $union) {
$this->union[] = $union;
}
}
}
/**
* Sets the ON condition for a relational query.
* The condition will be used in the ON part when [[ActiveQuery::joinWith()]] is called.
* Otherwise, the condition will be used in the WHERE part of a query.
*
* Use this method to specify additional conditions when declaring a relation in the [[ActiveRecord]] class:
*
* ```php
* public function getActiveUsers()
* {
* return $this->hasMany(User::className(), ['id' => 'user_id'])->onCondition(['active' => true]);
* }
* ```
*
* @param string|array $condition the ON condition. Please refer to [[Query::where()]] on how to specify this parameter.
* @param array $params the parameters (name => value) to be bound to the query.
* @return static the query object itself
*/
public function onCondition($condition, $params = [])
{
$this->on = $condition;
$this->addParams($params);
return $this;
}
/**
* Adds an additional ON condition to the existing one.
* The new condition and the existing one will be joined using the 'AND' operator.
* @param string|array $condition the new ON condition. Please refer to [[where()]]
* on how to specify this parameter.
* @return static the query object itself
* @see onCondition()
* @see orOnCondition()
*/
public function andOnCondition($condition, $params = [])
{
if ($this->on === null) {
$this->on = $condition;
} else {
$this->on = ['and', $this->on, $condition];
}
$this->addParams($params);
return $this;
}
/**
* Adds an additional ON condition to the existing one.
* The new condition and the existing one will be joined using the 'OR' operator.
* @param string|array $condition the new ON condition. Please refer to [[where()]]
* on how to specify this parameter.
* @return static the query object itself
* @see onCondition()
* @see andOnCondition()
*/
public function orOnCondition($condition, $params = [])
{
if ($this->on === null) {
$this->on = $condition;
} else {
$this->on = ['or', $this->on, $condition];
}
$this->addParams($params);
return $this;
}
/**
* Specifies the pivot table for a relational query.
*
* Use this method to specify a pivot table when declaring a relation in the [[ActiveRecord]] class:
*
* ```php
* public function getItems()
* {
* return $this->hasMany(Item::className(), ['id' => 'item_id'])
* ->viaTable('order_item', ['order_id' => 'id']);
* }
* ```
*
* @param string $tableName the name of the pivot table.
* @param array $link the link between the pivot table and the table associated with [[primaryModel]].
* The keys of the array represent the columns in the pivot table, and the values represent the columns
* in the [[primaryModel]] table.
* @param callable $callable a PHP callback for customizing the relation associated with the pivot table.
* Its signature should be `function($query)`, where `$query` is the query to be customized.
* @return static
* @see via()
*/
public function viaTable($tableName, $link, $callable = null)
{
$relation = new ActiveQuery(get_class($this->primaryModel), [
'from' => [$tableName],
'link' => $link,
'multiple' => true,
'asArray' => true,
]);
$this->via = $relation;
if ($callable !== null) {
call_user_func($callable, $relation);
}
return $this;
}
}