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 junction 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 * @author Carsten Brandt * @since 2.0 */ class ActiveQuery extends Query implements ActiveQueryInterface { use ActiveQueryTrait; use ActiveRelationTrait; /** * @event Event an event that is triggered when the query is initialized via [[init()]]. */ const EVENT_INIT = 'init'; /** * @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 string $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); } /** * Initializes the object. * This method is called at the end of the constructor. The default implementation will trigger * an [[EVENT_INIT]] event. If you override this method, make sure you call the parent implementation at the end * to ensure triggering of the event. */ public function init() { parent::init(); $this->trigger(self::EVENT_INIT); } /** * 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 prepare($builder) { // NOTE: because the same ActiveQuery may be used to build different SQL statements // (e.g. by ActiveDataProvider, one for count query, the other for row data query, // it is important to make sure the same ActiveQuery can be used to build SQL statements // multiple times. 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)) { $this->from = [$this->getPrimaryTableName()]; } if (empty($this->select) && !empty($this->join)) { list(, $alias) = $this->getTableNameAndAlias(); $this->select = ["$alias.*"]; } if ($this->primaryModel === null) { // eager loading $query = Query::create($this); } else { // lazy loading of a relation $where = $this->where; if ($this->via instanceof self) { // via junction table $viaModels = $this->via->findJunctionRows([$this->primaryModel]); $this->filterByModels($viaModels); } elseif (is_array($this->via)) { // via relation /* @var $viaQuery ActiveQuery */ list($viaName, $viaQuery, $viaCallableUsed) = $this->via; if ($viaQuery->multiple) { if ($viaCallableUsed) { $viaModels = $viaQuery->all(); } elseif ($this->primaryModel->isRelationPopulated($viaName)) { $viaModels = $this->primaryModel->$viaName; } else { $viaModels = $viaQuery->all(); $this->primaryModel->populateRelation($viaName, $viaModels); } } else { if ($viaCallableUsed) { $model = $viaQuery->one(); } elseif ($this->primaryModel->isRelationPopulated($viaName)) { $model = $this->primaryModel->$viaName; } else { $model = $viaQuery->one(); $this->primaryModel->populateRelation($viaName, $model); } $viaModels = $model === null ? [] : [$model]; } $this->filterByModels($viaModels); } else { $this->filterByModels([$this->primaryModel]); } $query = Query::create($this); $this->where = $where; } if (!empty($this->on)) { $query->andWhere($this->on); } return $query; } /** * {@inheritdoc} */ public function populate($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->inverseOf !== null) { $this->addInverseRelations($models); } if (!$this->asArray) { foreach ($models as $model) { $model->afterFind(); } } return parent::populate($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 * @throws InvalidConfigException if model primary key is empty * @return array the distinctive models */ private function removeDuplicatedModels($models) { $hash = []; /* @var $class ActiveRecord */ $class = $this->modelClass; $pks = $class::primaryKey(); if (count($pks) > 1) { // composite primary key foreach ($models as $i => $model) { $key = []; foreach ($pks as $pk) { if (!isset($model[$pk])) { // do not continue if the primary key is not part of the result set break 2; } $key[] = $model[$pk]; } $key = serialize($key); if (isset($hash[$key])) { unset($models[$i]); } else { $hash[$key] = true; } } } elseif (empty($pks)) { throw new InvalidConfigException("Primary key of '{$class}' can not be empty."); } else { // single column primary key $pk = reset($pks); foreach ($models as $i => $model) { if (!isset($model[$pk])) { // do not continue if the primary key is not part of the result set break; } $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|null $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) { $row = parent::one($db); if ($row !== false) { $models = $this->populate([$row]); return reset($models) ?: null; } return null; } /** * 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. */ public function createCommand($db = null) { /* @var $modelClass ActiveRecord */ $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; } $command = $db->createCommand($sql, $params); $this->setCommandCache($command); return $command; } /** * {@inheritdoc} */ protected function queryScalar($selectExpression, $db) { /* @var $modelClass ActiveRecord */ $modelClass = $this->modelClass; if ($db === null) { $db = $modelClass::getDb(); } if ($this->sql === null) { return parent::queryScalar($selectExpression, $db); } $command = (new Query())->select([$selectExpression]) ->from(['c' => "({$this->sql})"]) ->params($this->params) ->createCommand($db); $this->setCommandCache($command); return $command->queryScalar(); } /** * 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 perform eager loading for 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 string|array $with the relations to be joined. This can either be a string, representing a relation name or * an array with the following semantics: * * - Each array element represents a single relation. * - You may specify the relation name as the array key and provide an anonymous functions that * can be used to modify the relation queries on-the-fly as the array value. * - If a relation query does not need modification, you may use the relation name as the array value. * * The relation name may optionally contain an alias for the relation table (e.g. `books b`). * * Sub-relations can also be specified, see [[with()]] for the syntax. * * In the following you find some examples: * * ```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 (\yii\db\ActiveQuery $query) { * $query->orderBy('item.name'); * } * ])->all(); * // find all orders that contain books of the category 'Science fiction', using the alias "b" for the books table * Order::find()->joinWith(['books b'], true, 'INNER JOIN')->where(['b.category' => 'Science fiction'])->all(); * ``` * * The alias syntax is available since version 2.0.7. * * @param bool|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. Note, that this does * not mean, that the relations are populated from the query result. An * extra query will still be performed to bring in the related data. * Defaults to `true`. * @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 $this the query object itself */ public function joinWith($with, $eagerLoading = true, $joinType = 'LEFT JOIN') { $relations = []; foreach ((array) $with as $name => $callback) { if (is_int($name)) { $name = $callback; $callback = null; } if (preg_match('/^(.*?)(?:\s+AS\s+|\s+)(\w+)$/i', $name, $matches)) { // relation is defined with an alias, adjust callback to apply alias list(, $relation, $alias) = $matches; $name = $relation; $callback = function ($query) use ($callback, $alias) { /* @var $query ActiveQuery */ $query->alias($alias); if ($callback !== null) { call_user_func($callback, $query); } }; } if ($callback === null) { $relations[] = $name; } else { $relations[$name] = $callback; } } $this->joinWith[] = [$relations, $eagerLoading, $joinType]; return $this; } private function buildJoinWith() { $join = $this->join; $this->join = []; /* @var $modelClass ActiveRecordInterface */ $modelClass = $this->modelClass; $model = $modelClass::instance(); foreach ($this->joinWith as $config) { list($with, $eagerLoading, $joinType) = $config; $this->joinWithRelations($model, $with, $joinType); if (is_array($eagerLoading)) { foreach ($with as $name => $callback) { if (is_int($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 string|array $with the relations to be joined with. * @param bool|array $eagerLoading whether to eager load the relations. * Note, that this does not mean, that the relations are populated from the * query result. An extra query will still be performed to bring in the * related data. * @return $this 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_int($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]; } /* @var $relationModelClass ActiveRecordInterface */ $relationModelClass = $relation->modelClass; $primaryModel = $relationModelClass::instance(); $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); } if (!empty($relation->joinWith)) { $relation->buildJoinWith(); } $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]; } return is_string($joinType) ? $joinType : 'INNER JOIN'; } /** * Returns the table name and the table alias for [[modelClass]]. * @return array the table name and the table alias. * @since 2.0.16 */ protected function getTableNameAndAlias() { if (empty($this->from)) { $tableName = $this->getPrimaryTableName(); } else { $tableName = ''; // if the first entry in "from" is an alias-tablename-pair return it directly foreach ($this->from as $alias => $tableName) { if (is_string($alias)) { return [$tableName, $alias]; } 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 self) { // 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) = $parent->getTableNameAndAlias(); list($childTable, $childAlias) = $child->getTableNameAndAlias(); 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]); * } * ``` * * Note that this condition is applied in case of a join as well as when fetching the related records. * Thus only fields of the related table can be used in the condition. Trying to access fields of the primary * record will cause an error in a non-join-query. * * @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 $this 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. * @param array $params the parameters (name => value) to be bound to the query. * @return $this 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. * @param array $params the parameters (name => value) to be bound to the query. * @return $this 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 junction table for a relational query. * * Use this method to specify a junction 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 junction table. * @param array $link the link between the junction table and the table associated with [[primaryModel]]. * The keys of the array represent the columns in the junction table, and the values represent the columns * in the [[primaryModel]] table. * @param callable $callable a PHP callback for customizing the relation associated with the junction table. * Its signature should be `function($query)`, where `$query` is the query to be customized. * @return $this the query object itself * @throws InvalidConfigException when query is not initialized properly * @see via() */ public function viaTable($tableName, $link, callable $callable = null) { $modelClass = $this->primaryModel ? get_class($this->primaryModel) : $this->modelClass; $relation = new self($modelClass, [ 'from' => [$tableName], 'link' => $link, 'multiple' => true, 'asArray' => true, ]); $this->via = $relation; if ($callable !== null) { call_user_func($callable, $relation); } return $this; } /** * Define an alias for the table defined in [[modelClass]]. * * This method will adjust [[from]] so that an already defined alias will be overwritten. * If none was defined, [[from]] will be populated with the given alias. * * @param string $alias the table alias. * @return $this the query object itself * @since 2.0.7 */ public function alias($alias) { if (empty($this->from) || count($this->from) < 2) { list($tableName) = $this->getTableNameAndAlias(); $this->from = [$alias => $tableName]; } else { $tableName = $this->getPrimaryTableName(); foreach ($this->from as $key => $table) { if ($table === $tableName) { unset($this->from[$key]); $this->from[$alias] = $tableName; } } } return $this; } /** * {@inheritdoc} * @since 2.0.12 */ public function getTablesUsedInFrom() { if (empty($this->from)) { return $this->cleanUpTableNames([$this->getPrimaryTableName()]); } return parent::getTablesUsedInFrom(); } /** * @return string primary table name * @since 2.0.12 */ protected function getPrimaryTableName() { /* @var $modelClass ActiveRecord */ $modelClass = $this->modelClass; return $modelClass::tableName(); } }