From 4653526262030cf75799212440861b902e00d761 Mon Sep 17 00:00:00 2001 From: Carsten Brandt Date: Tue, 29 Oct 2013 12:20:52 +0100 Subject: [PATCH 01/10] moved common parts of db ActiveRelation and Query to traits these parts can be reused in other ActiveRecord implementations --- framework/yii/ar/ActiveQuery.php | 234 ++++++++++++++++++++++++++++ framework/yii/ar/ActiveRelation.php | 253 ++++++++++++++++++++++++++++++ framework/yii/db/ActiveQuery.php | 188 +---------------------- framework/yii/db/ActiveRelation.php | 221 +------------------------- framework/yii/db/BaseQuery.php | 299 ++++++++++++++++++++++++++++++++++++ framework/yii/db/Query.php | 128 +-------------- 6 files changed, 792 insertions(+), 531 deletions(-) create mode 100644 framework/yii/ar/ActiveQuery.php create mode 100644 framework/yii/ar/ActiveRelation.php create mode 100644 framework/yii/db/BaseQuery.php diff --git a/framework/yii/ar/ActiveQuery.php b/framework/yii/ar/ActiveQuery.php new file mode 100644 index 0000000..729b7b4 --- /dev/null +++ b/framework/yii/ar/ActiveQuery.php @@ -0,0 +1,234 @@ +with('orders')->asArray()->all(); + * ~~~ + * + * @author Qiang Xue + * @author Carsten Brandt + * @since 2.0 + */ +trait ActiveQuery +{ + /** + * @var string the name of the ActiveRecord class. + */ + public $modelClass; + /** + * @var array list of relations that this query should be performed with + */ + public $with; + /** + * @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; + + + /** + * 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); + call_user_func_array([$this->modelClass, $name], $params); + return $this; + } else { + return parent::__call($name, $params); + } + } + + /** + * Sets the [[asArray]] property. + * @param boolean $value whether to return the query results in terms of arrays instead of Active Records. + * @return static the query object itself + */ + public function asArray($value = true) + { + $this->asArray = $value; + return $this; + } + + /** + * 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([ + * 'orders' => function($query) { + * $query->andWhere('status = 1'); + * }, + * 'country', + * ])->all(); + * ~~~ + * + * @return static the query object itself + */ + 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; + } + + /** + * Sets the [[indexBy]] property. + * @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 + * } + * ~~~ + * + * @return static the query object itself + */ + public function indexBy($column) + { + return parent::indexBy($column); + } + + private function createModels($rows) + { + $models = []; + if ($this->asArray) { + if ($this->indexBy === null) { + return $rows; + } + foreach ($rows as $row) { + if (is_string($this->indexBy)) { + $key = $row[$this->indexBy]; + } else { + $key = call_user_func($this->indexBy, $row); + } + $models[$key] = $row; + } + } else { + /** @var $class ActiveRecord */ + $class = $this->modelClass; + if ($this->indexBy === null) { + foreach ($rows as $row) { + $models[] = $class::create($row); + } + } else { + foreach ($rows as $row) { + $model = $class::create($row); + if (is_string($this->indexBy)) { + $key = $model->{$this->indexBy}; + } else { + $key = call_user_func($this->indexBy, $model); + } + $models[$key] = $model; + } + } + } + return $models; + } + + private function populateRelations(&$models, $with) + { + $primaryModel = new $this->modelClass; + $relations = $this->normalizeRelations($primaryModel, $with); + foreach ($relations as $name => $relation) { + if ($relation->asArray === null) { + // inherit asArray from primary query + $relation->asArray = $this->asArray; + } + $relation->findWith($name, $models); + } + } + + /** + * @param ActiveRecord $model + * @param array $with + * @return ActiveRelation[] + */ + private function normalizeRelations($model, $with) + { + $relations = []; + foreach ($with as $name => $callback) { + if (is_integer($name)) { + $name = $callback; + $callback = null; + } + if (($pos = strpos($name, '.')) !== false) { + // with sub-relations + $childName = substr($name, $pos + 1); + $name = substr($name, 0, $pos); + } else { + $childName = null; + } + + $t = strtolower($name); + if (!isset($relations[$t])) { + $relation = $model->getRelation($name); + $relation->primaryModel = null; + $relations[$t] = $relation; + } else { + $relation = $relations[$t]; + } + + if (isset($childName)) { + $relation->with[$childName] = $callback; + } elseif ($callback !== null) { + call_user_func($callback, $relation); + } + } + return $relations; + } +} diff --git a/framework/yii/ar/ActiveRelation.php b/framework/yii/ar/ActiveRelation.php new file mode 100644 index 0000000..17bba67 --- /dev/null +++ b/framework/yii/ar/ActiveRelation.php @@ -0,0 +1,253 @@ + + * @since 2.0 + */ +trait ActiveRelation +{ + /** + * @var boolean whether this relation should populate all query results into AR instances. + * If false, only the first row of the results will be retrieved. + */ + public $multiple; + /** + * @var ActiveRecord the primary model that this relation is associated with. + * This is used only in lazy loading with dynamic query options. + */ + public $primaryModel; + /** + * @var array the columns of the primary and foreign tables that establish the relation. + * The array keys must be columns of the table for this relation, and the array values + * must be the corresponding columns from the primary table. + * Do not prefix or quote the column names as they will be done automatically by Yii. + */ + public $link; + /** + * @var array the query associated with the pivot table. Please call [[via()]] + * to set this property instead of directly setting it. + */ + public $via; + + /** + * Clones internal objects. + */ + public function __clone() + { + if (is_object($this->via)) { + // make a clone of "via" object so that the same query object can be reused multiple times + $this->via = clone $this->via; + } + } + + /** + * Specifies the relation associated with the pivot table. + * @param string $relationName the relation name. This refers to a relation declared in [[primaryModel]]. + * @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 the relation object itself. + */ + public function via($relationName, $callable = null) + { + $relation = $this->primaryModel->getRelation($relationName); + $this->via = [$relationName, $relation]; + if ($callable !== null) { + call_user_func($callable, $relation); + } + return $this; + } + + /** + * Finds the related records and populates them into the primary models. + * This method is internally used by [[ActiveQuery]]. Do not call it directly. + * @param string $name the relation name + * @param array $primaryModels primary models + * @return array the related models + * @throws InvalidConfigException + */ + public function findWith($name, &$primaryModels) + { + if (!is_array($this->link)) { + throw new InvalidConfigException('Invalid link: it must be an array of key-value pairs.'); + } + + if ($this->via instanceof self) { + // via pivot table + /** @var $viaQuery ActiveRelation */ + $viaQuery = $this->via; + $viaModels = $viaQuery->findPivotRows($primaryModels); + $this->filterByModels($viaModels); + } elseif (is_array($this->via)) { + // via relation + /** @var $viaQuery ActiveRelation */ + list($viaName, $viaQuery) = $this->via; + $viaQuery->primaryModel = null; + $viaModels = $viaQuery->findWith($viaName, $primaryModels); + $this->filterByModels($viaModels); + } else { + $this->filterByModels($primaryModels); + } + + if (count($primaryModels) === 1 && !$this->multiple) { + $model = $this->one(); + foreach ($primaryModels as $i => $primaryModel) { + if ($primaryModel instanceof ActiveRecord) { + $primaryModel->populateRelation($name, $model); + } else { + $primaryModels[$i][$name] = $model; + } + } + return [$model]; + } else { + $models = $this->all(); + if (isset($viaModels, $viaQuery)) { + $buckets = $this->buildBuckets($models, $this->link, $viaModels, $viaQuery->link); + } else { + $buckets = $this->buildBuckets($models, $this->link); + } + + $link = array_values(isset($viaQuery) ? $viaQuery->link : $this->link); + foreach ($primaryModels as $i => $primaryModel) { + $key = $this->getModelKey($primaryModel, $link); + $value = isset($buckets[$key]) ? $buckets[$key] : ($this->multiple ? [] : null); + if ($primaryModel instanceof ActiveRecord) { + $primaryModel->populateRelation($name, $value); + } else { + $primaryModels[$i][$name] = $value; + } + } + return $models; + } + } + + /** + * @param array $models + * @param array $link + * @param array $viaModels + * @param array $viaLink + * @return array + */ + private function buildBuckets($models, $link, $viaModels = null, $viaLink = null) + { + $buckets = []; + $linkKeys = array_keys($link); + foreach ($models as $i => $model) { + $key = $this->getModelKey($model, $linkKeys); + if ($this->indexBy !== null) { + $buckets[$key][$i] = $model; + } else { + $buckets[$key][] = $model; + } + } + + if ($viaModels !== null) { + $viaBuckets = []; + $viaLinkKeys = array_keys($viaLink); + $linkValues = array_values($link); + foreach ($viaModels as $viaModel) { + $key1 = $this->getModelKey($viaModel, $viaLinkKeys); + $key2 = $this->getModelKey($viaModel, $linkValues); + if (isset($buckets[$key2])) { + foreach ($buckets[$key2] as $i => $bucket) { + if ($this->indexBy !== null) { + $viaBuckets[$key1][$i] = $bucket; + } else { + $viaBuckets[$key1][] = $bucket; + } + } + } + } + $buckets = $viaBuckets; + } + + if (!$this->multiple) { + foreach ($buckets as $i => $bucket) { + $buckets[$i] = reset($bucket); + } + } + return $buckets; + } + + /** + * @param ActiveRecord|array $model + * @param array $attributes + * @return string + */ + private function getModelKey($model, $attributes) + { + if (count($attributes) > 1) { + $key = []; + foreach ($attributes as $attribute) { + $key[] = $model[$attribute]; + } + return serialize($key); + } else { + $attribute = reset($attributes); + return $model[$attribute]; + } + } + + /** + * @param array $models + */ + private function filterByModels($models) + { + $attributes = array_keys($this->link); + $values = []; + if (count($attributes) === 1) { + // single key + $attribute = reset($this->link); + foreach ($models as $model) { + if (($value = $model[$attribute]) !== null) { + $values[] = $value; + } + } + } else { + // composite keys + foreach ($models as $model) { + $v = []; + foreach ($this->link as $attribute => $link) { + $v[$attribute] = $model[$link]; + } + $values[] = $v; + } + } + $this->andWhere(['in', $attributes, array_unique($values, SORT_REGULAR)]); + } + + /** + * @param ActiveRecord[] $primaryModels + * @return array + */ + private function findPivotRows($primaryModels) + { + if (empty($primaryModels)) { + return []; + } + $this->filterByModels($primaryModels); + /** @var $primaryModel ActiveRecord */ + $primaryModel = reset($primaryModels); + return $this->asArray()->all($primaryModel->getDb()); + } +} diff --git a/framework/yii/db/ActiveQuery.php b/framework/yii/db/ActiveQuery.php index b7fcdcd..5ec3d7a 100644 --- a/framework/yii/db/ActiveQuery.php +++ b/framework/yii/db/ActiveQuery.php @@ -46,19 +46,8 @@ namespace yii\db; */ class ActiveQuery extends Query { - /** - * @var string the name of the ActiveRecord class. - */ - public $modelClass; - /** - * @var array list of relations that this query should be performed with - */ - public $with; - /** - * @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; + use \yii\ar\ActiveQuery; + /** * @var string the SQL statement to be executed for retrieving AR records. * This is set by [[ActiveRecord::findBySql()]]. @@ -67,25 +56,6 @@ class ActiveQuery extends Query /** - * 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); - call_user_func_array([$this->modelClass, $name], $params); - return $this; - } else { - return parent::__call($name, $params); - } - } - - /** * 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. @@ -164,158 +134,4 @@ class ActiveQuery extends Query } return $db->createCommand($this->sql, $params); } - - /** - * Sets the [[asArray]] property. - * @param boolean $value whether to return the query results in terms of arrays instead of Active Records. - * @return static the query object itself - */ - public function asArray($value = true) - { - $this->asArray = $value; - return $this; - } - - /** - * 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([ - * 'orders' => function($query) { - * $query->andWhere('status = 1'); - * }, - * 'country', - * ])->all(); - * ~~~ - * - * @return static the query object itself - */ - 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; - } - - /** - * Sets the [[indexBy]] property. - * @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 - * } - * ~~~ - * - * @return static the query object itself - */ - public function indexBy($column) - { - return parent::indexBy($column); - } - - private function createModels($rows) - { - $models = []; - if ($this->asArray) { - if ($this->indexBy === null) { - return $rows; - } - foreach ($rows as $row) { - if (is_string($this->indexBy)) { - $key = $row[$this->indexBy]; - } else { - $key = call_user_func($this->indexBy, $row); - } - $models[$key] = $row; - } - } else { - /** @var $class ActiveRecord */ - $class = $this->modelClass; - if ($this->indexBy === null) { - foreach ($rows as $row) { - $models[] = $class::create($row); - } - } else { - foreach ($rows as $row) { - $model = $class::create($row); - if (is_string($this->indexBy)) { - $key = $model->{$this->indexBy}; - } else { - $key = call_user_func($this->indexBy, $model); - } - $models[$key] = $model; - } - } - } - return $models; - } - - private function populateRelations(&$models, $with) - { - $primaryModel = new $this->modelClass; - $relations = $this->normalizeRelations($primaryModel, $with); - foreach ($relations as $name => $relation) { - if ($relation->asArray === null) { - // inherit asArray from primary query - $relation->asArray = $this->asArray; - } - $relation->findWith($name, $models); - } - } - - /** - * @param ActiveRecord $model - * @param array $with - * @return ActiveRelation[] - */ - private function normalizeRelations($model, $with) - { - $relations = []; - foreach ($with as $name => $callback) { - if (is_integer($name)) { - $name = $callback; - $callback = null; - } - if (($pos = strpos($name, '.')) !== false) { - // with sub-relations - $childName = substr($name, $pos + 1); - $name = substr($name, 0, $pos); - } else { - $childName = null; - } - - $t = strtolower($name); - if (!isset($relations[$t])) { - $relation = $model->getRelation($name); - $relation->primaryModel = null; - $relations[$t] = $relation; - } else { - $relation = $relations[$t]; - } - - if (isset($childName)) { - $relation->with[$childName] = $callback; - } elseif ($callback !== null) { - call_user_func($callback, $relation); - } - } - return $relations; - } } diff --git a/framework/yii/db/ActiveRelation.php b/framework/yii/db/ActiveRelation.php index 1a7541a..3714c6a 100644 --- a/framework/yii/db/ActiveRelation.php +++ b/framework/yii/db/ActiveRelation.php @@ -27,56 +27,14 @@ use yii\base\InvalidConfigException; */ class ActiveRelation extends ActiveQuery { - /** - * @var boolean whether this relation should populate all query results into AR instances. - * If false, only the first row of the results will be retrieved. - */ - public $multiple; - /** - * @var ActiveRecord the primary model that this relation is associated with. - * This is used only in lazy loading with dynamic query options. - */ - public $primaryModel; - /** - * @var array the columns of the primary and foreign tables that establish the relation. - * The array keys must be columns of the table for this relation, and the array values - * must be the corresponding columns from the primary table. - * Do not prefix or quote the column names as they will be done automatically by Yii. - */ - public $link; + use \yii\ar\ActiveRelation; + /** * @var array|ActiveRelation the query associated with the pivot table. Please call [[via()]] * or [[viaTable()]] to set this property instead of directly setting it. */ public $via; - /** - * Clones internal objects. - */ - public function __clone() - { - if (is_object($this->via)) { - // make a clone of "via" object so that the same query object can be reused multiple times - $this->via = clone $this->via; - } - } - - /** - * Specifies the relation associated with the pivot table. - * @param string $relationName the relation name. This refers to a relation declared in [[primaryModel]]. - * @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 the relation object itself. - */ - public function via($relationName, $callable = null) - { - $relation = $this->primaryModel->getRelation($relationName); - $this->via = [$relationName, $relation]; - if ($callable !== null) { - call_user_func($callable, $relation); - } - return $this; - } /** * Specifies the pivot table. @@ -137,179 +95,4 @@ class ActiveRelation extends ActiveQuery } return parent::createCommand($db); } - - /** - * Finds the related records and populates them into the primary models. - * This method is internally used by [[ActiveQuery]]. Do not call it directly. - * @param string $name the relation name - * @param array $primaryModels primary models - * @return array the related models - * @throws InvalidConfigException - */ - public function findWith($name, &$primaryModels) - { - if (!is_array($this->link)) { - throw new InvalidConfigException('Invalid link: it must be an array of key-value pairs.'); - } - - if ($this->via instanceof self) { - // via pivot table - /** @var $viaQuery ActiveRelation */ - $viaQuery = $this->via; - $viaModels = $viaQuery->findPivotRows($primaryModels); - $this->filterByModels($viaModels); - } elseif (is_array($this->via)) { - // via relation - /** @var $viaQuery ActiveRelation */ - list($viaName, $viaQuery) = $this->via; - $viaQuery->primaryModel = null; - $viaModels = $viaQuery->findWith($viaName, $primaryModels); - $this->filterByModels($viaModels); - } else { - $this->filterByModels($primaryModels); - } - - if (count($primaryModels) === 1 && !$this->multiple) { - $model = $this->one(); - foreach ($primaryModels as $i => $primaryModel) { - if ($primaryModel instanceof ActiveRecord) { - $primaryModel->populateRelation($name, $model); - } else { - $primaryModels[$i][$name] = $model; - } - } - return [$model]; - } else { - $models = $this->all(); - if (isset($viaModels, $viaQuery)) { - $buckets = $this->buildBuckets($models, $this->link, $viaModels, $viaQuery->link); - } else { - $buckets = $this->buildBuckets($models, $this->link); - } - - $link = array_values(isset($viaQuery) ? $viaQuery->link : $this->link); - foreach ($primaryModels as $i => $primaryModel) { - $key = $this->getModelKey($primaryModel, $link); - $value = isset($buckets[$key]) ? $buckets[$key] : ($this->multiple ? [] : null); - if ($primaryModel instanceof ActiveRecord) { - $primaryModel->populateRelation($name, $value); - } else { - $primaryModels[$i][$name] = $value; - } - } - return $models; - } - } - - /** - * @param array $models - * @param array $link - * @param array $viaModels - * @param array $viaLink - * @return array - */ - private function buildBuckets($models, $link, $viaModels = null, $viaLink = null) - { - $buckets = []; - $linkKeys = array_keys($link); - foreach ($models as $i => $model) { - $key = $this->getModelKey($model, $linkKeys); - if ($this->indexBy !== null) { - $buckets[$key][$i] = $model; - } else { - $buckets[$key][] = $model; - } - } - - if ($viaModels !== null) { - $viaBuckets = []; - $viaLinkKeys = array_keys($viaLink); - $linkValues = array_values($link); - foreach ($viaModels as $viaModel) { - $key1 = $this->getModelKey($viaModel, $viaLinkKeys); - $key2 = $this->getModelKey($viaModel, $linkValues); - if (isset($buckets[$key2])) { - foreach ($buckets[$key2] as $i => $bucket) { - if ($this->indexBy !== null) { - $viaBuckets[$key1][$i] = $bucket; - } else { - $viaBuckets[$key1][] = $bucket; - } - } - } - } - $buckets = $viaBuckets; - } - - if (!$this->multiple) { - foreach ($buckets as $i => $bucket) { - $buckets[$i] = reset($bucket); - } - } - return $buckets; - } - - /** - * @param ActiveRecord|array $model - * @param array $attributes - * @return string - */ - private function getModelKey($model, $attributes) - { - if (count($attributes) > 1) { - $key = []; - foreach ($attributes as $attribute) { - $key[] = $model[$attribute]; - } - return serialize($key); - } else { - $attribute = reset($attributes); - return $model[$attribute]; - } - } - - /** - * @param array $models - */ - private function filterByModels($models) - { - $attributes = array_keys($this->link); - $values = []; - if (count($attributes) === 1) { - // single key - $attribute = reset($this->link); - foreach ($models as $model) { - if (($value = $model[$attribute]) !== null) { - $values[] = $value; - } - } - } else { - // composite keys - foreach ($models as $model) { - $v = []; - foreach ($this->link as $attribute => $link) { - $v[$attribute] = $model[$link]; - } - $values[] = $v; - } - } - $this->andWhere(['in', $attributes, array_unique($values, SORT_REGULAR)]); - } - - /** - * @param ActiveRecord[] $primaryModels - * @return array - */ - private function findPivotRows($primaryModels) - { - if (empty($primaryModels)) { - return []; - } - $this->filterByModels($primaryModels); - /** @var $primaryModel ActiveRecord */ - $primaryModel = reset($primaryModels); - $db = $primaryModel->getDb(); - list ($sql, $params) = $db->getQueryBuilder()->build($this); - return $db->createCommand($sql, $params)->queryAll(); - } } diff --git a/framework/yii/db/BaseQuery.php b/framework/yii/db/BaseQuery.php new file mode 100644 index 0000000..98fcdab --- /dev/null +++ b/framework/yii/db/BaseQuery.php @@ -0,0 +1,299 @@ + + * @author Carsten Brandt + * @since 2.0 + */ +trait BaseQuery +{ + /** + * @var string|array query condition. This refers to the WHERE clause in a SQL statement. + * For example, `age > 31 AND team = 1`. + * @see where() + */ + public $where; + /** + * @var integer maximum number of records to be returned. If not set or less than 0, it means no limit. + */ + public $limit; + /** + * @var integer zero-based offset from where the records are to be returned. If not set or + * less than 0, it means starting from the beginning. + */ + public $offset; + /** + * @var array how to sort the query results. This is used to construct the ORDER BY clause in a SQL statement. + * The array keys are the columns to be sorted by, and the array values are the corresponding sort directions which + * can be either [[Query::SORT_ASC]] or [[Query::SORT_DESC]]. The array may also contain [[Expression]] objects. + * If that is the case, the expressions will be converted into strings without any change. + */ + public $orderBy; + /** + * @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 data. For more details, see [[indexBy()]]. This property is only used by [[all()]]. + */ + public $indexBy; + + /** + * Sets the [[indexBy]] property. + * @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 data. The signature of the callable should be: + * + * ~~~ + * function ($row) + * { + * // return the index value corresponding to $row + * } + * ~~~ + * + * @return static the query object itself + */ + public function indexBy($column) + { + $this->indexBy = $column; + return $this; + } + + /** + * Executes the query and returns all results as an array. + * @return array the query results. If the query results in nothing, an empty array will be returned. + */ + abstract public function all(); + + /** + * Executes the query and returns a single row of result. + * @return array|boolean the first row (in terms of an array) of the query result. False is returned if the query + * results in nothing. + */ + abstract public function one(); + + /** + * Returns the number of records. + * @return integer number of records + */ + abstract public function count(); + + /** + * Returns a value indicating whether the query result contains any row of data. + * @return boolean whether the query result contains any row of data. + */ + abstract public function exists(); + + /** + * Sets the WHERE part of the query. + * + * The method requires a $condition parameter. + * + * The $condition parameter should be an array in one of the following two formats: + * + * - hash format: `['column1' => value1, 'column2' => value2, ...]` + * - operator format: `[operator, operand1, operand2, ...]` + * + * A condition in hash format represents the following SQL expression in general: + * `column1=value1 AND column2=value2 AND ...`. In case when a value is an array, + * an `IN` expression will be generated. And if a value is null, `IS NULL` will be used + * in the generated expression. Below are some examples: + * + * - `['type' => 1, 'status' => 2]` generates `(type = 1) AND (status = 2)`. + * - `['id' => [1, 2, 3], 'status' => 2]` generates `(id IN (1, 2, 3)) AND (status = 2)`. + * - `['status' => null] generates `status IS NULL`. + * + * A condition in operator format generates the SQL expression according to the specified operator, which + * can be one of the followings: + * + * - `and`: the operands should be concatenated together using `AND`. For example, + * `['and', 'id=1', 'id=2']` will generate `id=1 AND id=2`. If an operand is an array, + * it will be converted into a string using the rules described here. For example, + * `['and', 'type=1', ['or', 'id=1', 'id=2']]` will generate `type=1 AND (id=1 OR id=2)`. + * The method will NOT do any quoting or escaping. + * + * - `or`: similar to the `and` operator except that the operands are concatenated using `OR`. + * + * - `between`: operand 1 should be the column name, and operand 2 and 3 should be the + * starting and ending values of the range that the column is in. + * For example, `['between', 'id', 1, 10]` will generate `id BETWEEN 1 AND 10`. + * + * - `not between`: similar to `between` except the `BETWEEN` is replaced with `NOT BETWEEN` + * in the generated condition. + * + * - `in`: operand 1 should be a column or DB expression, and operand 2 be an array representing + * the range of the values that the column or DB expression should be in. For example, + * `['in', 'id', [1, 2, 3]]` will generate `id IN (1, 2, 3)`. + * The method will properly quote the column name and escape values in the range. + * + * - `not in`: similar to the `in` operator except that `IN` is replaced with `NOT IN` in the generated condition. + * + * - `like`: operand 1 should be a column or DB expression, and operand 2 be a string or an array representing + * the values that the column or DB expression should be like. + * For example, `['like', 'name', '%tester%']` will generate `name LIKE '%tester%'`. + * When the value range is given as an array, multiple `LIKE` predicates will be generated and concatenated + * using `AND`. For example, `['like', 'name', ['%test%', '%sample%']]` will generate + * `name LIKE '%test%' AND name LIKE '%sample%'`. + * The method will properly quote the column name and escape values in the range. + * + * - `or like`: similar to the `like` operator except that `OR` is used to concatenate the `LIKE` + * predicates when operand 2 is an array. + * + * - `not like`: similar to the `like` operator except that `LIKE` is replaced with `NOT LIKE` + * in the generated condition. + * + * - `or not like`: similar to the `not like` operator except that `OR` is used to concatenate + * the `NOT LIKE` predicates. + * + * @param array $condition the conditions that should be put in the WHERE part. + * @return static the query object itself + * @see andWhere() + * @see orWhere() + */ + public function where($condition) + { + $this->where = $condition; + return $this; + } + + /** + * Adds an additional WHERE 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 WHERE condition. Please refer to [[where()]] + * on how to specify this parameter. + * @return static the query object itself + * @see where() + * @see orWhere() + */ + public function andWhere($condition) + { + if ($this->where === null) { + $this->where = $condition; + } else { + $this->where = ['and', $this->where, $condition]; + } + return $this; + } + + /** + * Adds an additional WHERE 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 WHERE condition. Please refer to [[where()]] + * on how to specify this parameter. + * @return static the query object itself + * @see where() + * @see andWhere() + */ + public function orWhere($condition) + { + if ($this->where === null) { + $this->where = $condition; + } else { + $this->where = ['or', $this->where, $condition]; + } + return $this; + } + + /** + * Sets the ORDER BY part of the query. + * @param string|array $columns the columns (and the directions) to be ordered by. + * Columns can be specified in either a string (e.g. "id ASC, name DESC") or an array + * (e.g. `['id' => Query::SORT_ASC, 'name' => Query::SORT_DESC]`). + * The method will automatically quote the column names unless a column contains some parenthesis + * (which means the column contains a DB expression). + * @return static the query object itself + * @see addOrderBy() + */ + public function orderBy($columns) + { + $this->orderBy = $this->normalizeOrderBy($columns); + return $this; + } + + /** + * Adds additional ORDER BY columns to the query. + * @param string|array $columns the columns (and the directions) to be ordered by. + * Columns can be specified in either a string (e.g. "id ASC, name DESC") or an array + * (e.g. `['id' => Query::SORT_ASC, 'name' => Query::SORT_DESC]`). + * The method will automatically quote the column names unless a column contains some parenthesis + * (which means the column contains a DB expression). + * @return static the query object itself + * @see orderBy() + */ + public function addOrderBy($columns) + { + $columns = $this->normalizeOrderBy($columns); + if ($this->orderBy === null) { + $this->orderBy = $columns; + } else { + $this->orderBy = array_merge($this->orderBy, $columns); + } + return $this; + } + + protected function normalizeOrderBy($columns) + { + if (is_array($columns)) { + return $columns; + } else { + $columns = preg_split('/\s*,\s*/', trim($columns), -1, PREG_SPLIT_NO_EMPTY); + $result = []; + foreach ($columns as $column) { + if (preg_match('/^(.*?)\s+(asc|desc)$/i', $column, $matches)) { + $result[$matches[1]] = strcasecmp($matches[2], 'desc') ? self::SORT_ASC : self::SORT_DESC; + } else { + $result[$column] = self::SORT_ASC; + } + } + return $result; + } + } + + /** + * Sets the LIMIT part of the query. + * @param integer $limit the limit. Use null or negative value to disable limit. + * @return static the query object itself + */ + public function limit($limit) + { + $this->limit = $limit; + return $this; + } + + /** + * Sets the OFFSET part of the query. + * @param integer $offset the offset. Use null or negative value to disable offset. + * @return static the query object itself + */ + public function offset($offset) + { + $this->offset = $offset; + return $this; + } +} \ No newline at end of file diff --git a/framework/yii/db/Query.php b/framework/yii/db/Query.php index 0839849..73f8860 100644 --- a/framework/yii/db/Query.php +++ b/framework/yii/db/Query.php @@ -37,6 +37,8 @@ use yii\base\Component; */ class Query extends Component { + use BaseQuery; + /** * Sort ascending * @see orderBy @@ -71,28 +73,6 @@ class Query extends Component */ public $from; /** - * @var string|array query condition. This refers to the WHERE clause in a SQL statement. - * For example, `age > 31 AND team = 1`. - * @see where() - */ - public $where; - /** - * @var integer maximum number of records to be returned. If not set or less than 0, it means no limit. - */ - public $limit; - /** - * @var integer zero-based offset from where the records are to be returned. If not set or - * less than 0, it means starting from the beginning. - */ - public $offset; - /** - * @var array how to sort the query results. This is used to construct the ORDER BY clause in a SQL statement. - * The array keys are the columns to be sorted by, and the array values are the corresponding sort directions which - * can be either [[Query::SORT_ASC]] or [[Query::SORT_DESC]]. The array may also contain [[Expression]] objects. - * If that is the case, the expressions will be converted into strings without any change. - */ - public $orderBy; - /** * @var array how to group the query results. For example, `['company', 'department']`. * This is used to construct the GROUP BY clause in a SQL statement. */ @@ -130,12 +110,6 @@ class Query extends Component * For example, `[':name' => 'Dan', ':age' => 31]`. */ public $params; - /** - * @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 data. For more details, see [[indexBy()]]. This property is only used by [[all()]]. - */ - public $indexBy; /** @@ -154,27 +128,6 @@ class Query extends Component } /** - * Sets the [[indexBy]] property. - * @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 data. The signature of the callable should be: - * - * ~~~ - * function ($row) - * { - * // return the index value corresponding to $row - * } - * ~~~ - * - * @return static the query object itself - */ - public function indexBy($column) - { - $this->indexBy = $column; - return $this; - } - - /** * Executes the query and returns all results as an array. * @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. @@ -653,83 +606,6 @@ class Query extends Component } /** - * Sets the ORDER BY part of the query. - * @param string|array $columns the columns (and the directions) to be ordered by. - * Columns can be specified in either a string (e.g. "id ASC, name DESC") or an array - * (e.g. `['id' => Query::SORT_ASC, 'name' => Query::SORT_DESC]`). - * The method will automatically quote the column names unless a column contains some parenthesis - * (which means the column contains a DB expression). - * @return static the query object itself - * @see addOrderBy() - */ - public function orderBy($columns) - { - $this->orderBy = $this->normalizeOrderBy($columns); - return $this; - } - - /** - * Adds additional ORDER BY columns to the query. - * @param string|array $columns the columns (and the directions) to be ordered by. - * Columns can be specified in either a string (e.g. "id ASC, name DESC") or an array - * (e.g. `['id' => Query::SORT_ASC, 'name' => Query::SORT_DESC]`). - * The method will automatically quote the column names unless a column contains some parenthesis - * (which means the column contains a DB expression). - * @return static the query object itself - * @see orderBy() - */ - public function addOrderBy($columns) - { - $columns = $this->normalizeOrderBy($columns); - if ($this->orderBy === null) { - $this->orderBy = $columns; - } else { - $this->orderBy = array_merge($this->orderBy, $columns); - } - return $this; - } - - protected function normalizeOrderBy($columns) - { - if (is_array($columns)) { - return $columns; - } else { - $columns = preg_split('/\s*,\s*/', trim($columns), -1, PREG_SPLIT_NO_EMPTY); - $result = []; - foreach ($columns as $column) { - if (preg_match('/^(.*?)\s+(asc|desc)$/i', $column, $matches)) { - $result[$matches[1]] = strcasecmp($matches[2], 'desc') ? self::SORT_ASC : self::SORT_DESC; - } else { - $result[$column] = self::SORT_ASC; - } - } - return $result; - } - } - - /** - * Sets the LIMIT part of the query. - * @param integer $limit the limit. Use null or negative value to disable limit. - * @return static the query object itself - */ - public function limit($limit) - { - $this->limit = $limit; - return $this; - } - - /** - * Sets the OFFSET part of the query. - * @param integer $offset the offset. Use null or negative value to disable offset. - * @return static the query object itself - */ - public function offset($offset) - { - $this->offset = $offset; - return $this; - } - - /** * Appends a SQL statement using UNION operator. * @param string|Query $sql the SQL statement to be appended using UNION * @return static the query object itself From 0146596647b359d5f9afafbd9cd58e1b955bc0ac Mon Sep 17 00:00:00 2001 From: Carsten Brandt Date: Tue, 29 Oct 2013 12:41:24 +0100 Subject: [PATCH 02/10] add relationClassName to AR to allow different relation classes ... for different dbms --- framework/yii/db/ActiveRecord.php | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/framework/yii/db/ActiveRecord.php b/framework/yii/db/ActiveRecord.php index 79d5146..be44a07 100644 --- a/framework/yii/db/ActiveRecord.php +++ b/framework/yii/db/ActiveRecord.php @@ -93,6 +93,10 @@ class ActiveRecord extends Model const OP_ALL = 0x07; /** + * @var string the name of the class to use for relations. + */ + protected $relationClassName = 'yii\\db\\ActiveRelation'; + /** * @var array attribute values indexed by attribute names */ private $_attributes = []; @@ -384,7 +388,7 @@ class ActiveRecord extends Model return $this->_related[$t]; } $value = parent::__get($name); - if ($value instanceof ActiveRelation) { + if ($value instanceof $this->relationClassName) { return $this->_related[$t] = $value->multiple ? $value->all() : $value->one(); } else { return $value; @@ -474,7 +478,7 @@ class ActiveRecord extends Model */ public function hasOne($class, $link) { - return new ActiveRelation([ + return new $this->relationClassName([ 'modelClass' => $class, 'primaryModel' => $this, 'link' => $link, @@ -512,7 +516,7 @@ class ActiveRecord extends Model */ public function hasMany($class, $link) { - return new ActiveRelation([ + return new $this->relationClassName([ 'modelClass' => $class, 'primaryModel' => $this, 'link' => $link, @@ -1264,7 +1268,7 @@ class ActiveRecord extends Model $getter = 'get' . $name; try { $relation = $this->$getter(); - if ($relation instanceof ActiveRelation) { + if ($relation instanceof $this->relationClassName) { return $relation; } } catch (UnknownMethodException $e) { From 3130aadb33652cae6326cd653f05b5a74da169dd Mon Sep 17 00:00:00 2001 From: Carsten Brandt Date: Tue, 29 Oct 2013 12:52:52 +0100 Subject: [PATCH 03/10] fixed conflicting property declaration --- framework/yii/db/ActiveRelation.php | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/framework/yii/db/ActiveRelation.php b/framework/yii/db/ActiveRelation.php index 3714c6a..8ca1baf 100644 --- a/framework/yii/db/ActiveRelation.php +++ b/framework/yii/db/ActiveRelation.php @@ -8,8 +8,6 @@ namespace yii\db; -use yii\base\InvalidConfigException; - /** * ActiveRelation represents a relation between two Active Record classes. * @@ -22,6 +20,9 @@ use yii\base\InvalidConfigException; * * If a relation involves a pivot table, it may be specified by [[via()]] or [[viaTable()]] method. * + * @property array|ActiveRelation $via the query associated with the pivot table. Please call [[via()]] + * or [[viaTable()]] to set this property instead of directly setting it. + * * @author Qiang Xue * @since 2.0 */ @@ -30,13 +31,6 @@ class ActiveRelation extends ActiveQuery use \yii\ar\ActiveRelation; /** - * @var array|ActiveRelation the query associated with the pivot table. Please call [[via()]] - * or [[viaTable()]] to set this property instead of directly setting it. - */ - public $via; - - - /** * Specifies the pivot table. * @param string $tableName the name of the pivot table. * @param array $link the link between the pivot table and the table associated with [[primaryModel]]. From b8e31d50bb8839f6613e643c99b8cddf494908ac Mon Sep 17 00:00:00 2001 From: Carsten Brandt Date: Wed, 13 Nov 2013 16:50:30 +0100 Subject: [PATCH 04/10] renamed AR trait classes --- framework/yii/ar/ActiveQuery.php | 234 ------------------------ framework/yii/ar/ActiveQueryTrait.php | 234 ++++++++++++++++++++++++ framework/yii/ar/ActiveRelation.php | 253 -------------------------- framework/yii/ar/ActiveRelationTrait.php | 253 ++++++++++++++++++++++++++ framework/yii/db/ActiveQuery.php | 2 +- framework/yii/db/ActiveRelation.php | 2 +- framework/yii/db/BaseQuery.php | 299 ------------------------------- framework/yii/db/Query.php | 2 +- framework/yii/db/QueryTrait.php | 299 +++++++++++++++++++++++++++++++ 9 files changed, 789 insertions(+), 789 deletions(-) delete mode 100644 framework/yii/ar/ActiveQuery.php create mode 100644 framework/yii/ar/ActiveQueryTrait.php delete mode 100644 framework/yii/ar/ActiveRelation.php create mode 100644 framework/yii/ar/ActiveRelationTrait.php delete mode 100644 framework/yii/db/BaseQuery.php create mode 100644 framework/yii/db/QueryTrait.php diff --git a/framework/yii/ar/ActiveQuery.php b/framework/yii/ar/ActiveQuery.php deleted file mode 100644 index 729b7b4..0000000 --- a/framework/yii/ar/ActiveQuery.php +++ /dev/null @@ -1,234 +0,0 @@ -with('orders')->asArray()->all(); - * ~~~ - * - * @author Qiang Xue - * @author Carsten Brandt - * @since 2.0 - */ -trait ActiveQuery -{ - /** - * @var string the name of the ActiveRecord class. - */ - public $modelClass; - /** - * @var array list of relations that this query should be performed with - */ - public $with; - /** - * @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; - - - /** - * 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); - call_user_func_array([$this->modelClass, $name], $params); - return $this; - } else { - return parent::__call($name, $params); - } - } - - /** - * Sets the [[asArray]] property. - * @param boolean $value whether to return the query results in terms of arrays instead of Active Records. - * @return static the query object itself - */ - public function asArray($value = true) - { - $this->asArray = $value; - return $this; - } - - /** - * 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([ - * 'orders' => function($query) { - * $query->andWhere('status = 1'); - * }, - * 'country', - * ])->all(); - * ~~~ - * - * @return static the query object itself - */ - 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; - } - - /** - * Sets the [[indexBy]] property. - * @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 - * } - * ~~~ - * - * @return static the query object itself - */ - public function indexBy($column) - { - return parent::indexBy($column); - } - - private function createModels($rows) - { - $models = []; - if ($this->asArray) { - if ($this->indexBy === null) { - return $rows; - } - foreach ($rows as $row) { - if (is_string($this->indexBy)) { - $key = $row[$this->indexBy]; - } else { - $key = call_user_func($this->indexBy, $row); - } - $models[$key] = $row; - } - } else { - /** @var $class ActiveRecord */ - $class = $this->modelClass; - if ($this->indexBy === null) { - foreach ($rows as $row) { - $models[] = $class::create($row); - } - } else { - foreach ($rows as $row) { - $model = $class::create($row); - if (is_string($this->indexBy)) { - $key = $model->{$this->indexBy}; - } else { - $key = call_user_func($this->indexBy, $model); - } - $models[$key] = $model; - } - } - } - return $models; - } - - private function populateRelations(&$models, $with) - { - $primaryModel = new $this->modelClass; - $relations = $this->normalizeRelations($primaryModel, $with); - foreach ($relations as $name => $relation) { - if ($relation->asArray === null) { - // inherit asArray from primary query - $relation->asArray = $this->asArray; - } - $relation->findWith($name, $models); - } - } - - /** - * @param ActiveRecord $model - * @param array $with - * @return ActiveRelation[] - */ - private function normalizeRelations($model, $with) - { - $relations = []; - foreach ($with as $name => $callback) { - if (is_integer($name)) { - $name = $callback; - $callback = null; - } - if (($pos = strpos($name, '.')) !== false) { - // with sub-relations - $childName = substr($name, $pos + 1); - $name = substr($name, 0, $pos); - } else { - $childName = null; - } - - $t = strtolower($name); - if (!isset($relations[$t])) { - $relation = $model->getRelation($name); - $relation->primaryModel = null; - $relations[$t] = $relation; - } else { - $relation = $relations[$t]; - } - - if (isset($childName)) { - $relation->with[$childName] = $callback; - } elseif ($callback !== null) { - call_user_func($callback, $relation); - } - } - return $relations; - } -} diff --git a/framework/yii/ar/ActiveQueryTrait.php b/framework/yii/ar/ActiveQueryTrait.php new file mode 100644 index 0000000..2100076 --- /dev/null +++ b/framework/yii/ar/ActiveQueryTrait.php @@ -0,0 +1,234 @@ +with('orders')->asArray()->all(); + * ~~~ + * + * @author Qiang Xue + * @author Carsten Brandt + * @since 2.0 + */ +trait ActiveQueryTrait +{ + /** + * @var string the name of the ActiveRecord class. + */ + public $modelClass; + /** + * @var array list of relations that this query should be performed with + */ + public $with; + /** + * @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; + + + /** + * 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); + call_user_func_array([$this->modelClass, $name], $params); + return $this; + } else { + return parent::__call($name, $params); + } + } + + /** + * Sets the [[asArray]] property. + * @param boolean $value whether to return the query results in terms of arrays instead of Active Records. + * @return static the query object itself + */ + public function asArray($value = true) + { + $this->asArray = $value; + return $this; + } + + /** + * 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([ + * 'orders' => function($query) { + * $query->andWhere('status = 1'); + * }, + * 'country', + * ])->all(); + * ~~~ + * + * @return static the query object itself + */ + 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; + } + + /** + * Sets the [[indexBy]] property. + * @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 + * } + * ~~~ + * + * @return static the query object itself + */ + public function indexBy($column) + { + return parent::indexBy($column); + } + + private function createModels($rows) + { + $models = []; + if ($this->asArray) { + if ($this->indexBy === null) { + return $rows; + } + foreach ($rows as $row) { + if (is_string($this->indexBy)) { + $key = $row[$this->indexBy]; + } else { + $key = call_user_func($this->indexBy, $row); + } + $models[$key] = $row; + } + } else { + /** @var $class ActiveRecord */ + $class = $this->modelClass; + if ($this->indexBy === null) { + foreach ($rows as $row) { + $models[] = $class::create($row); + } + } else { + foreach ($rows as $row) { + $model = $class::create($row); + if (is_string($this->indexBy)) { + $key = $model->{$this->indexBy}; + } else { + $key = call_user_func($this->indexBy, $model); + } + $models[$key] = $model; + } + } + } + return $models; + } + + private function populateRelations(&$models, $with) + { + $primaryModel = new $this->modelClass; + $relations = $this->normalizeRelations($primaryModel, $with); + foreach ($relations as $name => $relation) { + if ($relation->asArray === null) { + // inherit asArray from primary query + $relation->asArray = $this->asArray; + } + $relation->findWith($name, $models); + } + } + + /** + * @param ActiveRecord $model + * @param array $with + * @return ActiveRelation[] + */ + private function normalizeRelations($model, $with) + { + $relations = []; + foreach ($with as $name => $callback) { + if (is_integer($name)) { + $name = $callback; + $callback = null; + } + if (($pos = strpos($name, '.')) !== false) { + // with sub-relations + $childName = substr($name, $pos + 1); + $name = substr($name, 0, $pos); + } else { + $childName = null; + } + + $t = strtolower($name); + if (!isset($relations[$t])) { + $relation = $model->getRelation($name); + $relation->primaryModel = null; + $relations[$t] = $relation; + } else { + $relation = $relations[$t]; + } + + if (isset($childName)) { + $relation->with[$childName] = $callback; + } elseif ($callback !== null) { + call_user_func($callback, $relation); + } + } + return $relations; + } +} diff --git a/framework/yii/ar/ActiveRelation.php b/framework/yii/ar/ActiveRelation.php deleted file mode 100644 index 17bba67..0000000 --- a/framework/yii/ar/ActiveRelation.php +++ /dev/null @@ -1,253 +0,0 @@ - - * @since 2.0 - */ -trait ActiveRelation -{ - /** - * @var boolean whether this relation should populate all query results into AR instances. - * If false, only the first row of the results will be retrieved. - */ - public $multiple; - /** - * @var ActiveRecord the primary model that this relation is associated with. - * This is used only in lazy loading with dynamic query options. - */ - public $primaryModel; - /** - * @var array the columns of the primary and foreign tables that establish the relation. - * The array keys must be columns of the table for this relation, and the array values - * must be the corresponding columns from the primary table. - * Do not prefix or quote the column names as they will be done automatically by Yii. - */ - public $link; - /** - * @var array the query associated with the pivot table. Please call [[via()]] - * to set this property instead of directly setting it. - */ - public $via; - - /** - * Clones internal objects. - */ - public function __clone() - { - if (is_object($this->via)) { - // make a clone of "via" object so that the same query object can be reused multiple times - $this->via = clone $this->via; - } - } - - /** - * Specifies the relation associated with the pivot table. - * @param string $relationName the relation name. This refers to a relation declared in [[primaryModel]]. - * @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 the relation object itself. - */ - public function via($relationName, $callable = null) - { - $relation = $this->primaryModel->getRelation($relationName); - $this->via = [$relationName, $relation]; - if ($callable !== null) { - call_user_func($callable, $relation); - } - return $this; - } - - /** - * Finds the related records and populates them into the primary models. - * This method is internally used by [[ActiveQuery]]. Do not call it directly. - * @param string $name the relation name - * @param array $primaryModels primary models - * @return array the related models - * @throws InvalidConfigException - */ - public function findWith($name, &$primaryModels) - { - if (!is_array($this->link)) { - throw new InvalidConfigException('Invalid link: it must be an array of key-value pairs.'); - } - - if ($this->via instanceof self) { - // via pivot table - /** @var $viaQuery ActiveRelation */ - $viaQuery = $this->via; - $viaModels = $viaQuery->findPivotRows($primaryModels); - $this->filterByModels($viaModels); - } elseif (is_array($this->via)) { - // via relation - /** @var $viaQuery ActiveRelation */ - list($viaName, $viaQuery) = $this->via; - $viaQuery->primaryModel = null; - $viaModels = $viaQuery->findWith($viaName, $primaryModels); - $this->filterByModels($viaModels); - } else { - $this->filterByModels($primaryModels); - } - - if (count($primaryModels) === 1 && !$this->multiple) { - $model = $this->one(); - foreach ($primaryModels as $i => $primaryModel) { - if ($primaryModel instanceof ActiveRecord) { - $primaryModel->populateRelation($name, $model); - } else { - $primaryModels[$i][$name] = $model; - } - } - return [$model]; - } else { - $models = $this->all(); - if (isset($viaModels, $viaQuery)) { - $buckets = $this->buildBuckets($models, $this->link, $viaModels, $viaQuery->link); - } else { - $buckets = $this->buildBuckets($models, $this->link); - } - - $link = array_values(isset($viaQuery) ? $viaQuery->link : $this->link); - foreach ($primaryModels as $i => $primaryModel) { - $key = $this->getModelKey($primaryModel, $link); - $value = isset($buckets[$key]) ? $buckets[$key] : ($this->multiple ? [] : null); - if ($primaryModel instanceof ActiveRecord) { - $primaryModel->populateRelation($name, $value); - } else { - $primaryModels[$i][$name] = $value; - } - } - return $models; - } - } - - /** - * @param array $models - * @param array $link - * @param array $viaModels - * @param array $viaLink - * @return array - */ - private function buildBuckets($models, $link, $viaModels = null, $viaLink = null) - { - $buckets = []; - $linkKeys = array_keys($link); - foreach ($models as $i => $model) { - $key = $this->getModelKey($model, $linkKeys); - if ($this->indexBy !== null) { - $buckets[$key][$i] = $model; - } else { - $buckets[$key][] = $model; - } - } - - if ($viaModels !== null) { - $viaBuckets = []; - $viaLinkKeys = array_keys($viaLink); - $linkValues = array_values($link); - foreach ($viaModels as $viaModel) { - $key1 = $this->getModelKey($viaModel, $viaLinkKeys); - $key2 = $this->getModelKey($viaModel, $linkValues); - if (isset($buckets[$key2])) { - foreach ($buckets[$key2] as $i => $bucket) { - if ($this->indexBy !== null) { - $viaBuckets[$key1][$i] = $bucket; - } else { - $viaBuckets[$key1][] = $bucket; - } - } - } - } - $buckets = $viaBuckets; - } - - if (!$this->multiple) { - foreach ($buckets as $i => $bucket) { - $buckets[$i] = reset($bucket); - } - } - return $buckets; - } - - /** - * @param ActiveRecord|array $model - * @param array $attributes - * @return string - */ - private function getModelKey($model, $attributes) - { - if (count($attributes) > 1) { - $key = []; - foreach ($attributes as $attribute) { - $key[] = $model[$attribute]; - } - return serialize($key); - } else { - $attribute = reset($attributes); - return $model[$attribute]; - } - } - - /** - * @param array $models - */ - private function filterByModels($models) - { - $attributes = array_keys($this->link); - $values = []; - if (count($attributes) === 1) { - // single key - $attribute = reset($this->link); - foreach ($models as $model) { - if (($value = $model[$attribute]) !== null) { - $values[] = $value; - } - } - } else { - // composite keys - foreach ($models as $model) { - $v = []; - foreach ($this->link as $attribute => $link) { - $v[$attribute] = $model[$link]; - } - $values[] = $v; - } - } - $this->andWhere(['in', $attributes, array_unique($values, SORT_REGULAR)]); - } - - /** - * @param ActiveRecord[] $primaryModels - * @return array - */ - private function findPivotRows($primaryModels) - { - if (empty($primaryModels)) { - return []; - } - $this->filterByModels($primaryModels); - /** @var $primaryModel ActiveRecord */ - $primaryModel = reset($primaryModels); - return $this->asArray()->all($primaryModel->getDb()); - } -} diff --git a/framework/yii/ar/ActiveRelationTrait.php b/framework/yii/ar/ActiveRelationTrait.php new file mode 100644 index 0000000..ab80558 --- /dev/null +++ b/framework/yii/ar/ActiveRelationTrait.php @@ -0,0 +1,253 @@ + + * @since 2.0 + */ +trait ActiveRelationTrait +{ + /** + * @var boolean whether this relation should populate all query results into AR instances. + * If false, only the first row of the results will be retrieved. + */ + public $multiple; + /** + * @var ActiveRecord the primary model that this relation is associated with. + * This is used only in lazy loading with dynamic query options. + */ + public $primaryModel; + /** + * @var array the columns of the primary and foreign tables that establish the relation. + * The array keys must be columns of the table for this relation, and the array values + * must be the corresponding columns from the primary table. + * Do not prefix or quote the column names as they will be done automatically by Yii. + */ + public $link; + /** + * @var array the query associated with the pivot table. Please call [[via()]] + * to set this property instead of directly setting it. + */ + public $via; + + /** + * Clones internal objects. + */ + public function __clone() + { + if (is_object($this->via)) { + // make a clone of "via" object so that the same query object can be reused multiple times + $this->via = clone $this->via; + } + } + + /** + * Specifies the relation associated with the pivot table. + * @param string $relationName the relation name. This refers to a relation declared in [[primaryModel]]. + * @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 the relation object itself. + */ + public function via($relationName, $callable = null) + { + $relation = $this->primaryModel->getRelation($relationName); + $this->via = [$relationName, $relation]; + if ($callable !== null) { + call_user_func($callable, $relation); + } + return $this; + } + + /** + * Finds the related records and populates them into the primary models. + * This method is internally used by [[ActiveQuery]]. Do not call it directly. + * @param string $name the relation name + * @param array $primaryModels primary models + * @return array the related models + * @throws InvalidConfigException + */ + public function findWith($name, &$primaryModels) + { + if (!is_array($this->link)) { + throw new InvalidConfigException('Invalid link: it must be an array of key-value pairs.'); + } + + if ($this->via instanceof self) { + // via pivot table + /** @var $viaQuery ActiveRelation */ + $viaQuery = $this->via; + $viaModels = $viaQuery->findPivotRows($primaryModels); + $this->filterByModels($viaModels); + } elseif (is_array($this->via)) { + // via relation + /** @var $viaQuery ActiveRelation */ + list($viaName, $viaQuery) = $this->via; + $viaQuery->primaryModel = null; + $viaModels = $viaQuery->findWith($viaName, $primaryModels); + $this->filterByModels($viaModels); + } else { + $this->filterByModels($primaryModels); + } + + if (count($primaryModels) === 1 && !$this->multiple) { + $model = $this->one(); + foreach ($primaryModels as $i => $primaryModel) { + if ($primaryModel instanceof ActiveRecord) { + $primaryModel->populateRelation($name, $model); + } else { + $primaryModels[$i][$name] = $model; + } + } + return [$model]; + } else { + $models = $this->all(); + if (isset($viaModels, $viaQuery)) { + $buckets = $this->buildBuckets($models, $this->link, $viaModels, $viaQuery->link); + } else { + $buckets = $this->buildBuckets($models, $this->link); + } + + $link = array_values(isset($viaQuery) ? $viaQuery->link : $this->link); + foreach ($primaryModels as $i => $primaryModel) { + $key = $this->getModelKey($primaryModel, $link); + $value = isset($buckets[$key]) ? $buckets[$key] : ($this->multiple ? [] : null); + if ($primaryModel instanceof ActiveRecord) { + $primaryModel->populateRelation($name, $value); + } else { + $primaryModels[$i][$name] = $value; + } + } + return $models; + } + } + + /** + * @param array $models + * @param array $link + * @param array $viaModels + * @param array $viaLink + * @return array + */ + private function buildBuckets($models, $link, $viaModels = null, $viaLink = null) + { + $buckets = []; + $linkKeys = array_keys($link); + foreach ($models as $i => $model) { + $key = $this->getModelKey($model, $linkKeys); + if ($this->indexBy !== null) { + $buckets[$key][$i] = $model; + } else { + $buckets[$key][] = $model; + } + } + + if ($viaModels !== null) { + $viaBuckets = []; + $viaLinkKeys = array_keys($viaLink); + $linkValues = array_values($link); + foreach ($viaModels as $viaModel) { + $key1 = $this->getModelKey($viaModel, $viaLinkKeys); + $key2 = $this->getModelKey($viaModel, $linkValues); + if (isset($buckets[$key2])) { + foreach ($buckets[$key2] as $i => $bucket) { + if ($this->indexBy !== null) { + $viaBuckets[$key1][$i] = $bucket; + } else { + $viaBuckets[$key1][] = $bucket; + } + } + } + } + $buckets = $viaBuckets; + } + + if (!$this->multiple) { + foreach ($buckets as $i => $bucket) { + $buckets[$i] = reset($bucket); + } + } + return $buckets; + } + + /** + * @param ActiveRecord|array $model + * @param array $attributes + * @return string + */ + private function getModelKey($model, $attributes) + { + if (count($attributes) > 1) { + $key = []; + foreach ($attributes as $attribute) { + $key[] = $model[$attribute]; + } + return serialize($key); + } else { + $attribute = reset($attributes); + return $model[$attribute]; + } + } + + /** + * @param array $models + */ + private function filterByModels($models) + { + $attributes = array_keys($this->link); + $values = []; + if (count($attributes) === 1) { + // single key + $attribute = reset($this->link); + foreach ($models as $model) { + if (($value = $model[$attribute]) !== null) { + $values[] = $value; + } + } + } else { + // composite keys + foreach ($models as $model) { + $v = []; + foreach ($this->link as $attribute => $link) { + $v[$attribute] = $model[$link]; + } + $values[] = $v; + } + } + $this->andWhere(['in', $attributes, array_unique($values, SORT_REGULAR)]); + } + + /** + * @param ActiveRecord[] $primaryModels + * @return array + */ + private function findPivotRows($primaryModels) + { + if (empty($primaryModels)) { + return []; + } + $this->filterByModels($primaryModels); + /** @var $primaryModel ActiveRecord */ + $primaryModel = reset($primaryModels); + return $this->asArray()->all($primaryModel->getDb()); + } +} diff --git a/framework/yii/db/ActiveQuery.php b/framework/yii/db/ActiveQuery.php index 5ec3d7a..3b245fe 100644 --- a/framework/yii/db/ActiveQuery.php +++ b/framework/yii/db/ActiveQuery.php @@ -46,7 +46,7 @@ namespace yii\db; */ class ActiveQuery extends Query { - use \yii\ar\ActiveQuery; + use \yii\ar\ActiveQueryTrait; /** * @var string the SQL statement to be executed for retrieving AR records. diff --git a/framework/yii/db/ActiveRelation.php b/framework/yii/db/ActiveRelation.php index 8ca1baf..72b5667 100644 --- a/framework/yii/db/ActiveRelation.php +++ b/framework/yii/db/ActiveRelation.php @@ -28,7 +28,7 @@ namespace yii\db; */ class ActiveRelation extends ActiveQuery { - use \yii\ar\ActiveRelation; + use \yii\ar\ActiveRelationTrait; /** * Specifies the pivot table. diff --git a/framework/yii/db/BaseQuery.php b/framework/yii/db/BaseQuery.php deleted file mode 100644 index 98fcdab..0000000 --- a/framework/yii/db/BaseQuery.php +++ /dev/null @@ -1,299 +0,0 @@ - - * @author Carsten Brandt - * @since 2.0 - */ -trait BaseQuery -{ - /** - * @var string|array query condition. This refers to the WHERE clause in a SQL statement. - * For example, `age > 31 AND team = 1`. - * @see where() - */ - public $where; - /** - * @var integer maximum number of records to be returned. If not set or less than 0, it means no limit. - */ - public $limit; - /** - * @var integer zero-based offset from where the records are to be returned. If not set or - * less than 0, it means starting from the beginning. - */ - public $offset; - /** - * @var array how to sort the query results. This is used to construct the ORDER BY clause in a SQL statement. - * The array keys are the columns to be sorted by, and the array values are the corresponding sort directions which - * can be either [[Query::SORT_ASC]] or [[Query::SORT_DESC]]. The array may also contain [[Expression]] objects. - * If that is the case, the expressions will be converted into strings without any change. - */ - public $orderBy; - /** - * @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 data. For more details, see [[indexBy()]]. This property is only used by [[all()]]. - */ - public $indexBy; - - /** - * Sets the [[indexBy]] property. - * @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 data. The signature of the callable should be: - * - * ~~~ - * function ($row) - * { - * // return the index value corresponding to $row - * } - * ~~~ - * - * @return static the query object itself - */ - public function indexBy($column) - { - $this->indexBy = $column; - return $this; - } - - /** - * Executes the query and returns all results as an array. - * @return array the query results. If the query results in nothing, an empty array will be returned. - */ - abstract public function all(); - - /** - * Executes the query and returns a single row of result. - * @return array|boolean the first row (in terms of an array) of the query result. False is returned if the query - * results in nothing. - */ - abstract public function one(); - - /** - * Returns the number of records. - * @return integer number of records - */ - abstract public function count(); - - /** - * Returns a value indicating whether the query result contains any row of data. - * @return boolean whether the query result contains any row of data. - */ - abstract public function exists(); - - /** - * Sets the WHERE part of the query. - * - * The method requires a $condition parameter. - * - * The $condition parameter should be an array in one of the following two formats: - * - * - hash format: `['column1' => value1, 'column2' => value2, ...]` - * - operator format: `[operator, operand1, operand2, ...]` - * - * A condition in hash format represents the following SQL expression in general: - * `column1=value1 AND column2=value2 AND ...`. In case when a value is an array, - * an `IN` expression will be generated. And if a value is null, `IS NULL` will be used - * in the generated expression. Below are some examples: - * - * - `['type' => 1, 'status' => 2]` generates `(type = 1) AND (status = 2)`. - * - `['id' => [1, 2, 3], 'status' => 2]` generates `(id IN (1, 2, 3)) AND (status = 2)`. - * - `['status' => null] generates `status IS NULL`. - * - * A condition in operator format generates the SQL expression according to the specified operator, which - * can be one of the followings: - * - * - `and`: the operands should be concatenated together using `AND`. For example, - * `['and', 'id=1', 'id=2']` will generate `id=1 AND id=2`. If an operand is an array, - * it will be converted into a string using the rules described here. For example, - * `['and', 'type=1', ['or', 'id=1', 'id=2']]` will generate `type=1 AND (id=1 OR id=2)`. - * The method will NOT do any quoting or escaping. - * - * - `or`: similar to the `and` operator except that the operands are concatenated using `OR`. - * - * - `between`: operand 1 should be the column name, and operand 2 and 3 should be the - * starting and ending values of the range that the column is in. - * For example, `['between', 'id', 1, 10]` will generate `id BETWEEN 1 AND 10`. - * - * - `not between`: similar to `between` except the `BETWEEN` is replaced with `NOT BETWEEN` - * in the generated condition. - * - * - `in`: operand 1 should be a column or DB expression, and operand 2 be an array representing - * the range of the values that the column or DB expression should be in. For example, - * `['in', 'id', [1, 2, 3]]` will generate `id IN (1, 2, 3)`. - * The method will properly quote the column name and escape values in the range. - * - * - `not in`: similar to the `in` operator except that `IN` is replaced with `NOT IN` in the generated condition. - * - * - `like`: operand 1 should be a column or DB expression, and operand 2 be a string or an array representing - * the values that the column or DB expression should be like. - * For example, `['like', 'name', '%tester%']` will generate `name LIKE '%tester%'`. - * When the value range is given as an array, multiple `LIKE` predicates will be generated and concatenated - * using `AND`. For example, `['like', 'name', ['%test%', '%sample%']]` will generate - * `name LIKE '%test%' AND name LIKE '%sample%'`. - * The method will properly quote the column name and escape values in the range. - * - * - `or like`: similar to the `like` operator except that `OR` is used to concatenate the `LIKE` - * predicates when operand 2 is an array. - * - * - `not like`: similar to the `like` operator except that `LIKE` is replaced with `NOT LIKE` - * in the generated condition. - * - * - `or not like`: similar to the `not like` operator except that `OR` is used to concatenate - * the `NOT LIKE` predicates. - * - * @param array $condition the conditions that should be put in the WHERE part. - * @return static the query object itself - * @see andWhere() - * @see orWhere() - */ - public function where($condition) - { - $this->where = $condition; - return $this; - } - - /** - * Adds an additional WHERE 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 WHERE condition. Please refer to [[where()]] - * on how to specify this parameter. - * @return static the query object itself - * @see where() - * @see orWhere() - */ - public function andWhere($condition) - { - if ($this->where === null) { - $this->where = $condition; - } else { - $this->where = ['and', $this->where, $condition]; - } - return $this; - } - - /** - * Adds an additional WHERE 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 WHERE condition. Please refer to [[where()]] - * on how to specify this parameter. - * @return static the query object itself - * @see where() - * @see andWhere() - */ - public function orWhere($condition) - { - if ($this->where === null) { - $this->where = $condition; - } else { - $this->where = ['or', $this->where, $condition]; - } - return $this; - } - - /** - * Sets the ORDER BY part of the query. - * @param string|array $columns the columns (and the directions) to be ordered by. - * Columns can be specified in either a string (e.g. "id ASC, name DESC") or an array - * (e.g. `['id' => Query::SORT_ASC, 'name' => Query::SORT_DESC]`). - * The method will automatically quote the column names unless a column contains some parenthesis - * (which means the column contains a DB expression). - * @return static the query object itself - * @see addOrderBy() - */ - public function orderBy($columns) - { - $this->orderBy = $this->normalizeOrderBy($columns); - return $this; - } - - /** - * Adds additional ORDER BY columns to the query. - * @param string|array $columns the columns (and the directions) to be ordered by. - * Columns can be specified in either a string (e.g. "id ASC, name DESC") or an array - * (e.g. `['id' => Query::SORT_ASC, 'name' => Query::SORT_DESC]`). - * The method will automatically quote the column names unless a column contains some parenthesis - * (which means the column contains a DB expression). - * @return static the query object itself - * @see orderBy() - */ - public function addOrderBy($columns) - { - $columns = $this->normalizeOrderBy($columns); - if ($this->orderBy === null) { - $this->orderBy = $columns; - } else { - $this->orderBy = array_merge($this->orderBy, $columns); - } - return $this; - } - - protected function normalizeOrderBy($columns) - { - if (is_array($columns)) { - return $columns; - } else { - $columns = preg_split('/\s*,\s*/', trim($columns), -1, PREG_SPLIT_NO_EMPTY); - $result = []; - foreach ($columns as $column) { - if (preg_match('/^(.*?)\s+(asc|desc)$/i', $column, $matches)) { - $result[$matches[1]] = strcasecmp($matches[2], 'desc') ? self::SORT_ASC : self::SORT_DESC; - } else { - $result[$column] = self::SORT_ASC; - } - } - return $result; - } - } - - /** - * Sets the LIMIT part of the query. - * @param integer $limit the limit. Use null or negative value to disable limit. - * @return static the query object itself - */ - public function limit($limit) - { - $this->limit = $limit; - return $this; - } - - /** - * Sets the OFFSET part of the query. - * @param integer $offset the offset. Use null or negative value to disable offset. - * @return static the query object itself - */ - public function offset($offset) - { - $this->offset = $offset; - return $this; - } -} \ No newline at end of file diff --git a/framework/yii/db/Query.php b/framework/yii/db/Query.php index 73f8860..103cb67 100644 --- a/framework/yii/db/Query.php +++ b/framework/yii/db/Query.php @@ -37,7 +37,7 @@ use yii\base\Component; */ class Query extends Component { - use BaseQuery; + use QueryTrait; /** * Sort ascending diff --git a/framework/yii/db/QueryTrait.php b/framework/yii/db/QueryTrait.php new file mode 100644 index 0000000..92051ab --- /dev/null +++ b/framework/yii/db/QueryTrait.php @@ -0,0 +1,299 @@ + + * @author Carsten Brandt + * @since 2.0 + */ +trait QueryTrait +{ + /** + * @var string|array query condition. This refers to the WHERE clause in a SQL statement. + * For example, `age > 31 AND team = 1`. + * @see where() + */ + public $where; + /** + * @var integer maximum number of records to be returned. If not set or less than 0, it means no limit. + */ + public $limit; + /** + * @var integer zero-based offset from where the records are to be returned. If not set or + * less than 0, it means starting from the beginning. + */ + public $offset; + /** + * @var array how to sort the query results. This is used to construct the ORDER BY clause in a SQL statement. + * The array keys are the columns to be sorted by, and the array values are the corresponding sort directions which + * can be either [[Query::SORT_ASC]] or [[Query::SORT_DESC]]. The array may also contain [[Expression]] objects. + * If that is the case, the expressions will be converted into strings without any change. + */ + public $orderBy; + /** + * @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 data. For more details, see [[indexBy()]]. This property is only used by [[all()]]. + */ + public $indexBy; + + /** + * Sets the [[indexBy]] property. + * @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 data. The signature of the callable should be: + * + * ~~~ + * function ($row) + * { + * // return the index value corresponding to $row + * } + * ~~~ + * + * @return static the query object itself + */ + public function indexBy($column) + { + $this->indexBy = $column; + return $this; + } + + /** + * Executes the query and returns all results as an array. + * @return array the query results. If the query results in nothing, an empty array will be returned. + */ + abstract public function all(); + + /** + * Executes the query and returns a single row of result. + * @return array|boolean the first row (in terms of an array) of the query result. False is returned if the query + * results in nothing. + */ + abstract public function one(); + + /** + * Returns the number of records. + * @return integer number of records + */ + abstract public function count(); + + /** + * Returns a value indicating whether the query result contains any row of data. + * @return boolean whether the query result contains any row of data. + */ + abstract public function exists(); + + /** + * Sets the WHERE part of the query. + * + * The method requires a $condition parameter. + * + * The $condition parameter should be an array in one of the following two formats: + * + * - hash format: `['column1' => value1, 'column2' => value2, ...]` + * - operator format: `[operator, operand1, operand2, ...]` + * + * A condition in hash format represents the following SQL expression in general: + * `column1=value1 AND column2=value2 AND ...`. In case when a value is an array, + * an `IN` expression will be generated. And if a value is null, `IS NULL` will be used + * in the generated expression. Below are some examples: + * + * - `['type' => 1, 'status' => 2]` generates `(type = 1) AND (status = 2)`. + * - `['id' => [1, 2, 3], 'status' => 2]` generates `(id IN (1, 2, 3)) AND (status = 2)`. + * - `['status' => null] generates `status IS NULL`. + * + * A condition in operator format generates the SQL expression according to the specified operator, which + * can be one of the followings: + * + * - `and`: the operands should be concatenated together using `AND`. For example, + * `['and', 'id=1', 'id=2']` will generate `id=1 AND id=2`. If an operand is an array, + * it will be converted into a string using the rules described here. For example, + * `['and', 'type=1', ['or', 'id=1', 'id=2']]` will generate `type=1 AND (id=1 OR id=2)`. + * The method will NOT do any quoting or escaping. + * + * - `or`: similar to the `and` operator except that the operands are concatenated using `OR`. + * + * - `between`: operand 1 should be the column name, and operand 2 and 3 should be the + * starting and ending values of the range that the column is in. + * For example, `['between', 'id', 1, 10]` will generate `id BETWEEN 1 AND 10`. + * + * - `not between`: similar to `between` except the `BETWEEN` is replaced with `NOT BETWEEN` + * in the generated condition. + * + * - `in`: operand 1 should be a column or DB expression, and operand 2 be an array representing + * the range of the values that the column or DB expression should be in. For example, + * `['in', 'id', [1, 2, 3]]` will generate `id IN (1, 2, 3)`. + * The method will properly quote the column name and escape values in the range. + * + * - `not in`: similar to the `in` operator except that `IN` is replaced with `NOT IN` in the generated condition. + * + * - `like`: operand 1 should be a column or DB expression, and operand 2 be a string or an array representing + * the values that the column or DB expression should be like. + * For example, `['like', 'name', '%tester%']` will generate `name LIKE '%tester%'`. + * When the value range is given as an array, multiple `LIKE` predicates will be generated and concatenated + * using `AND`. For example, `['like', 'name', ['%test%', '%sample%']]` will generate + * `name LIKE '%test%' AND name LIKE '%sample%'`. + * The method will properly quote the column name and escape values in the range. + * + * - `or like`: similar to the `like` operator except that `OR` is used to concatenate the `LIKE` + * predicates when operand 2 is an array. + * + * - `not like`: similar to the `like` operator except that `LIKE` is replaced with `NOT LIKE` + * in the generated condition. + * + * - `or not like`: similar to the `not like` operator except that `OR` is used to concatenate + * the `NOT LIKE` predicates. + * + * @param array $condition the conditions that should be put in the WHERE part. + * @return static the query object itself + * @see andWhere() + * @see orWhere() + */ + public function where($condition) + { + $this->where = $condition; + return $this; + } + + /** + * Adds an additional WHERE 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 WHERE condition. Please refer to [[where()]] + * on how to specify this parameter. + * @return static the query object itself + * @see where() + * @see orWhere() + */ + public function andWhere($condition) + { + if ($this->where === null) { + $this->where = $condition; + } else { + $this->where = ['and', $this->where, $condition]; + } + return $this; + } + + /** + * Adds an additional WHERE 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 WHERE condition. Please refer to [[where()]] + * on how to specify this parameter. + * @return static the query object itself + * @see where() + * @see andWhere() + */ + public function orWhere($condition) + { + if ($this->where === null) { + $this->where = $condition; + } else { + $this->where = ['or', $this->where, $condition]; + } + return $this; + } + + /** + * Sets the ORDER BY part of the query. + * @param string|array $columns the columns (and the directions) to be ordered by. + * Columns can be specified in either a string (e.g. "id ASC, name DESC") or an array + * (e.g. `['id' => Query::SORT_ASC, 'name' => Query::SORT_DESC]`). + * The method will automatically quote the column names unless a column contains some parenthesis + * (which means the column contains a DB expression). + * @return static the query object itself + * @see addOrderBy() + */ + public function orderBy($columns) + { + $this->orderBy = $this->normalizeOrderBy($columns); + return $this; + } + + /** + * Adds additional ORDER BY columns to the query. + * @param string|array $columns the columns (and the directions) to be ordered by. + * Columns can be specified in either a string (e.g. "id ASC, name DESC") or an array + * (e.g. `['id' => Query::SORT_ASC, 'name' => Query::SORT_DESC]`). + * The method will automatically quote the column names unless a column contains some parenthesis + * (which means the column contains a DB expression). + * @return static the query object itself + * @see orderBy() + */ + public function addOrderBy($columns) + { + $columns = $this->normalizeOrderBy($columns); + if ($this->orderBy === null) { + $this->orderBy = $columns; + } else { + $this->orderBy = array_merge($this->orderBy, $columns); + } + return $this; + } + + protected function normalizeOrderBy($columns) + { + if (is_array($columns)) { + return $columns; + } else { + $columns = preg_split('/\s*,\s*/', trim($columns), -1, PREG_SPLIT_NO_EMPTY); + $result = []; + foreach ($columns as $column) { + if (preg_match('/^(.*?)\s+(asc|desc)$/i', $column, $matches)) { + $result[$matches[1]] = strcasecmp($matches[2], 'desc') ? self::SORT_ASC : self::SORT_DESC; + } else { + $result[$column] = self::SORT_ASC; + } + } + return $result; + } + } + + /** + * Sets the LIMIT part of the query. + * @param integer $limit the limit. Use null or negative value to disable limit. + * @return static the query object itself + */ + public function limit($limit) + { + $this->limit = $limit; + return $this; + } + + /** + * Sets the OFFSET part of the query. + * @param integer $offset the offset. Use null or negative value to disable offset. + * @return static the query object itself + */ + public function offset($offset) + { + $this->offset = $offset; + return $this; + } +} \ No newline at end of file From 65338972c7f7e6fee0e86bb300cc2502f42edcc1 Mon Sep 17 00:00:00 2001 From: Carsten Brandt Date: Wed, 13 Nov 2013 18:42:50 +0100 Subject: [PATCH 05/10] refactored ActiveRecord classes to use Interfaces and traits this allows us to implement other activerecord implementations based on NoSQL dbms --- docs/guide/query-builder.md | 4 +- framework/yii/ar/ActiveQueryInterface.php | 59 ++++++++ framework/yii/ar/ActiveQueryTrait.php | 16 ++- framework/yii/ar/ActiveRelationInterface.php | 36 +++++ framework/yii/ar/ActiveRelationTrait.php | 1 + framework/yii/data/ActiveDataProvider.php | 20 +-- framework/yii/db/ActiveQuery.php | 10 +- framework/yii/db/ActiveRecord.php | 28 ++-- framework/yii/db/ActiveRelation.php | 13 +- framework/yii/db/Query.php | 20 +-- framework/yii/db/QueryBuilder.php | 2 +- framework/yii/db/QueryInterface.php | 205 +++++++++++++++++++++++++++ framework/yii/db/QueryTrait.php | 109 ++------------ 13 files changed, 369 insertions(+), 154 deletions(-) create mode 100644 framework/yii/ar/ActiveQueryInterface.php create mode 100644 framework/yii/ar/ActiveRelationInterface.php create mode 100644 framework/yii/db/QueryInterface.php diff --git a/docs/guide/query-builder.md b/docs/guide/query-builder.md index 7625c0b..ac79f1d 100644 --- a/docs/guide/query-builder.md +++ b/docs/guide/query-builder.md @@ -167,8 +167,8 @@ For ordering results `orderBy` and `addOrderBy` could be used: ```php $query->orderBy([ - 'id' => Query::SORT_ASC, - 'name' => Query::SORT_DESC, + 'id' => SORT_ASC, + 'name' => SORT_DESC, ]); ``` diff --git a/framework/yii/ar/ActiveQueryInterface.php b/framework/yii/ar/ActiveQueryInterface.php new file mode 100644 index 0000000..e2bb780 --- /dev/null +++ b/framework/yii/ar/ActiveQueryInterface.php @@ -0,0 +1,59 @@ + + * @author Carsten Brandt + * @since 2.0 + */ +interface ActiveQueryInterface extends QueryInterface +{ + /** + * Sets the [[asArray]] property. + * @param boolean $value whether to return the query results in terms of arrays instead of Active Records. + * @return static the query object itself + */ + public function asArray($value = true); + + /** + * 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. + * + * A relation name can refer to a relation defined in [[modelClass]] + * or a sub-relation that stands for a relation of a related record. + * For example, `orders.address` means the `address` relation defined + * in the model class corresponding to the `orders` relation. + * + * 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 orders and the orders' shipping address + * Customer::find()->with('orders.address')->all(); + * // find customers together with their country and orders of status 1 + * Customer::find()->with([ + * 'orders' => function($query) { + * $query->andWhere('status = 1'); + * }, + * 'country', + * ])->all(); + * ~~~ + * + * @return static the query object itself + */ + public function with(); +} diff --git a/framework/yii/ar/ActiveQueryTrait.php b/framework/yii/ar/ActiveQueryTrait.php index 1a4336f..8c8d6c0 100644 --- a/framework/yii/ar/ActiveQueryTrait.php +++ b/framework/yii/ar/ActiveQueryTrait.php @@ -10,10 +10,7 @@ namespace yii\ar; use yii\db\ActiveRecord; /** - * ActiveQuery represents a DB query associated with an Active Record class. - * - * ActiveQuery instances are usually created by [[ActiveRecord::find()]], [[ActiveRecord::findBySql()]] - * and [[ActiveRecord::count()]]. + * ActiveQueryTrait implements the common set of methods that are used by DB queries associated with an Active Record class. * * ActiveQuery mainly provides the following methods to retrieve the query results: * @@ -150,6 +147,11 @@ trait ActiveQueryTrait return parent::indexBy($column); } + /** + * Converts found rows into model instances + * @param array $rows + * @return array|ActiveRecord[] + */ private function createModels($rows) { $models = []; @@ -187,6 +189,10 @@ trait ActiveQueryTrait return $models; } + /** + * @param ActiveRecord[] $models + * @param array $with + */ private function populateRelations(&$models, $with) { $primaryModel = new $this->modelClass; @@ -203,7 +209,7 @@ trait ActiveQueryTrait /** * @param ActiveRecord $model * @param array $with - * @return ActiveRelation[] + * @return ActiveRelationInterface[] */ private function normalizeRelations($model, $with) { diff --git a/framework/yii/ar/ActiveRelationInterface.php b/framework/yii/ar/ActiveRelationInterface.php new file mode 100644 index 0000000..70b723a --- /dev/null +++ b/framework/yii/ar/ActiveRelationInterface.php @@ -0,0 +1,36 @@ + + * @author Carsten Brandt + * @since 2.0 + */ +interface ActiveRelationInterface extends ActiveQueryInterface +{ + /** + * Specifies the relation associated with the pivot table. + * @param string $relationName the relation name. This refers to a relation declared in [[primaryModel]]. + * @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 the relation object itself. + */ + public function via($relationName, $callable = null); + + /** + * Finds the related records and populates them into the primary models. + * This method is internally used by [[ActiveQuery]]. Do not call it directly. + * @param string $name the relation name + * @param array $primaryModels primary models + * @return array the related models + */ + public function findWith($name, &$primaryModels); +} diff --git a/framework/yii/ar/ActiveRelationTrait.php b/framework/yii/ar/ActiveRelationTrait.php index 6d30ebc..350718d 100644 --- a/framework/yii/ar/ActiveRelationTrait.php +++ b/framework/yii/ar/ActiveRelationTrait.php @@ -23,6 +23,7 @@ use yii\db\ActiveRecord; * If a relation involves a pivot table, it may be specified by [[via()]] or [[viaTable()]] method. * * @author Qiang Xue + * @author Carsten Brandt * @since 2.0 */ trait ActiveRelationTrait diff --git a/framework/yii/data/ActiveDataProvider.php b/framework/yii/data/ActiveDataProvider.php index ca7696f..3766692 100644 --- a/framework/yii/data/ActiveDataProvider.php +++ b/framework/yii/data/ActiveDataProvider.php @@ -8,11 +8,11 @@ namespace yii\data; use Yii; +use yii\ar\ActiveQueryInterface; use yii\base\InvalidConfigException; use yii\base\Model; -use yii\db\Query; -use yii\db\ActiveQuery; use yii\db\Connection; +use yii\db\QueryInterface; /** * ActiveDataProvider implements a data provider based on [[Query]] and [[ActiveQuery]]. @@ -54,7 +54,7 @@ use yii\db\Connection; class ActiveDataProvider extends BaseDataProvider { /** - * @var Query the query that is used to fetch data models and [[totalCount]] + * @var QueryInterface the query that is used to fetch data models and [[totalCount]] * if it is not explicitly set. */ public $query; @@ -97,8 +97,8 @@ class ActiveDataProvider extends BaseDataProvider */ protected function prepareModels() { - if (!$this->query instanceof Query) { - throw new InvalidConfigException('The "query" property must be an instance of Query or its subclass.'); + if (!$this->query instanceof QueryInterface) { + throw new InvalidConfigException('The "query" property must be an instance of a class that implements the QueryInterface e.g. yii\db\Query or its subclasses.'); } if (($pagination = $this->getPagination()) !== false) { $pagination->totalCount = $this->getTotalCount(); @@ -125,7 +125,7 @@ class ActiveDataProvider extends BaseDataProvider } } return $keys; - } elseif ($this->query instanceof ActiveQuery) { + } elseif ($this->query instanceof ActiveQueryInterface) { /** @var \yii\db\ActiveRecord $class */ $class = $this->query->modelClass; $pks = $class::primaryKey(); @@ -154,11 +154,11 @@ class ActiveDataProvider extends BaseDataProvider */ protected function prepareTotalCount() { - if (!$this->query instanceof Query) { - throw new InvalidConfigException('The "query" property must be an instance of Query or its subclass.'); + if (!$this->query instanceof QueryInterface) { + throw new InvalidConfigException('The "query" property must be an instance of a class that implements the QueryInterface e.g. yii\db\Query or its subclasses.'); } $query = clone $this->query; - return (int) $query->limit(-1)->offset(-1)->count('*', $this->db); + return (int) $query->limit(-1)->offset(-1)->count($this->db); } /** @@ -167,7 +167,7 @@ class ActiveDataProvider extends BaseDataProvider public function setSort($value) { parent::setSort($value); - if (($sort = $this->getSort()) !== false && empty($sort->attributes) && $this->query instanceof ActiveQuery) { + if (($sort = $this->getSort()) !== false && empty($sort->attributes) && $this->query instanceof ActiveQueryInterface) { /** @var Model $model */ $model = new $this->query->modelClass; foreach ($model->attributes() as $attribute) { diff --git a/framework/yii/db/ActiveQuery.php b/framework/yii/db/ActiveQuery.php index 94494bc..3995b1b 100644 --- a/framework/yii/db/ActiveQuery.php +++ b/framework/yii/db/ActiveQuery.php @@ -7,12 +7,13 @@ */ namespace yii\db; +use yii\ar\ActiveQueryInterface; +use yii\ar\ActiveQueryTrait; /** * 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 instances are usually created by [[ActiveRecord::find()]] and [[ActiveRecord::findBySql()]]. * * ActiveQuery mainly provides the following methods to retrieve the query results: * @@ -42,11 +43,12 @@ namespace yii\db; * ~~~ * * @author Qiang Xue + * @author Carsten Brandt * @since 2.0 */ -class ActiveQuery extends Query +class ActiveQuery extends Query implements ActiveQueryInterface { - use \yii\ar\ActiveQueryTrait; + use ActiveQueryTrait; /** * @var string the SQL statement to be executed for retrieving AR records. diff --git a/framework/yii/db/ActiveRecord.php b/framework/yii/db/ActiveRecord.php index 57c5c8b..852d6f5 100644 --- a/framework/yii/db/ActiveRecord.php +++ b/framework/yii/db/ActiveRecord.php @@ -8,6 +8,7 @@ namespace yii\db; +use yii\ar\ActiveRelationInterface; use yii\base\InvalidConfigException; use yii\base\Model; use yii\base\InvalidParamException; @@ -36,6 +37,7 @@ use yii\helpers\Inflector; * the key value is null). This property is read-only. * * @author Qiang Xue + * @author Carsten Brandt * @since 2.0 */ class ActiveRecord extends Model @@ -95,10 +97,6 @@ class ActiveRecord extends Model const OP_ALL = 0x07; /** - * @var string the name of the class to use for relations. - */ - protected $relationClassName = 'yii\\db\\ActiveRelation'; - /** * @var array attribute values indexed by attribute names */ private $_attributes = []; @@ -256,7 +254,7 @@ class ActiveRecord extends Model /** * Creates an [[ActiveQuery]] instance. - * This method is called by [[find()]], [[findBySql()]] and [[count()]] to start a SELECT query. + * This method is called by [[find()]], [[findBySql()]] to start a SELECT query. * You may override this method to return a customized query (e.g. `CustomerQuery` specified * written for querying `Customer` purpose.) * @return ActiveQuery the newly created [[ActiveQuery]] instance. @@ -389,7 +387,7 @@ class ActiveRecord extends Model return $this->_related[$name]; } $value = parent::__get($name); - if ($value instanceof $this->relationClassName) { + if ($value instanceof ActiveRelationInterface) { return $this->_related[$name] = $value->multiple ? $value->all() : $value->one(); } else { return $value; @@ -478,7 +476,7 @@ class ActiveRecord extends Model */ public function hasOne($class, $link) { - return new $this->relationClassName([ + return $this->createActiveRelation([ 'modelClass' => $class, 'primaryModel' => $this, 'link' => $link, @@ -516,7 +514,7 @@ class ActiveRecord extends Model */ public function hasMany($class, $link) { - return new $this->relationClassName([ + return $this->createActiveRelation([ 'modelClass' => $class, 'primaryModel' => $this, 'link' => $link, @@ -525,6 +523,18 @@ class ActiveRecord extends Model } /** + * Creates an [[ActiveRelation]] instance. + * This method is called by [[hasOne()]] and [[hasMany()]] to create a relation instance. + * You may override this method to return a customized relation. + * @param array $config the configuration passed to the ActiveRelation class. + * @return ActiveRelation the newly created [[ActiveRelation]] instance. + */ + protected function createActiveRelation($config = []) + { + return new ActiveRelation($config); + } + + /** * Populates the named relation with the related records. * Note that this method does not check if the relation exists or not. * @param string $name the relation name (case-sensitive) @@ -1287,7 +1297,7 @@ class ActiveRecord extends Model $getter = 'get' . $name; try { $relation = $this->$getter(); - if ($relation instanceof $this->relationClassName) { + if ($relation instanceof ActiveRelationInterface) { return $relation; } else { return null; diff --git a/framework/yii/db/ActiveRelation.php b/framework/yii/db/ActiveRelation.php index 13ceb14..7bc449a 100644 --- a/framework/yii/db/ActiveRelation.php +++ b/framework/yii/db/ActiveRelation.php @@ -7,6 +7,8 @@ */ namespace yii\db; +use yii\ar\ActiveRelationInterface; +use yii\ar\ActiveRelationTrait; /** * ActiveRelation represents a relation between two Active Record classes. @@ -24,17 +26,12 @@ namespace yii\db; * or [[viaTable()]] to set this property instead of directly setting it. * * @author Qiang Xue + * @author Carsten Brandt * @since 2.0 */ -class ActiveRelation extends ActiveQuery +class ActiveRelation extends ActiveQuery implements ActiveRelationInterface { - use \yii\ar\ActiveRelationTrait; - - /** - * @var array|ActiveRelation the query associated with the pivot table. Please call [[via()]] - * or [[viaTable()]] to set this property instead of directly setting it. - */ - public $via; + use ActiveRelationTrait; /** * Specifies the pivot table. diff --git a/framework/yii/db/Query.php b/framework/yii/db/Query.php index 0a26fd9..0ae5f44 100644 --- a/framework/yii/db/Query.php +++ b/framework/yii/db/Query.php @@ -33,24 +33,14 @@ use yii\base\Component; * ~~~ * * @author Qiang Xue + * @author Carsten Brandt * @since 2.0 */ -class Query extends Component +class Query extends Component implements QueryInterface { use QueryTrait; /** - * Sort ascending - * @see orderBy() - */ - const SORT_ASC = false; - /** - * Sort descending - * @see orderBy() - */ - const SORT_DESC = true; - - /** * @var array the columns being selected. For example, `['id', 'name']`. * This is used to construct the SELECT clause in a SQL statement. If not set, if means selecting all columns. * @see select() @@ -189,13 +179,13 @@ class Query extends Component /** * Returns the number of records. + * @param Connection $db the database connection used to generate the SQL statement. + * If this parameter is not given (or null), the `db` application component will be used. * @param string $q the COUNT expression. Defaults to '*'. * 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 number of records */ - public function count($q = '*', $db = null) + public function count($db = null, $q = '*') { $this->select = ["COUNT($q)"]; return $this->createCommand($db)->queryScalar(); diff --git a/framework/yii/db/QueryBuilder.php b/framework/yii/db/QueryBuilder.php index 09948c8..0a547ae 100644 --- a/framework/yii/db/QueryBuilder.php +++ b/framework/yii/db/QueryBuilder.php @@ -683,7 +683,7 @@ class QueryBuilder extends \yii\base\Object if (is_object($direction)) { $orders[] = (string)$direction; } else { - $orders[] = $this->db->quoteColumnName($name) . ($direction === Query::SORT_DESC ? ' DESC' : ''); + $orders[] = $this->db->quoteColumnName($name) . ($direction === SORT_DESC ? ' DESC' : ''); } } diff --git a/framework/yii/db/QueryInterface.php b/framework/yii/db/QueryInterface.php new file mode 100644 index 0000000..d13fa2b --- /dev/null +++ b/framework/yii/db/QueryInterface.php @@ -0,0 +1,205 @@ + + * @author Carsten Brandt + * @since 2.0 + */ +interface QueryInterface +{ + /** + * Executes the query and returns all results as an array. + * @param Connection $db the database connection used to execute the query. + * If this parameter is not given, the `db` application component will be used. + * @return array the query results. If the query results in nothing, an empty array will be returned. + */ + public function all($db = null); + + /** + * Executes the query and returns a single row of result. + * @param Connection $db the database connection used to execute the query. + * If this parameter is not given, the `db` application component will be used. + * @return array|boolean the first row (in terms of an array) of the query result. False is returned if the query + * results in nothing. + */ + public function one($db = null); + + /** + * Returns the number of records. + * @param Connection $db the database connection used to execute the query. + * If this parameter is not given, the `db` application component will be used. + * @return integer number of records + */ + public function count($db = null); + + /** + * Returns a value indicating whether the query result contains any row of data. + * @param Connection $db the database connection used to execute the query. + * If this parameter is not given, the `db` application component will be used. + * @return boolean whether the query result contains any row of data. + */ + public function exists($db = null); + + /** + * Sets the [[indexBy]] property. + * @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 data. The signature of the callable should be: + * + * ~~~ + * function ($row) + * { + * // return the index value corresponding to $row + * } + * ~~~ + * + * @return static the query object itself + */ + public function indexBy($column); + + /** + * Sets the WHERE part of the query. + * + * The method requires a $condition parameter. + * + * The $condition parameter should be an array in one of the following two formats: + * + * - hash format: `['column1' => value1, 'column2' => value2, ...]` + * - operator format: `[operator, operand1, operand2, ...]` + * + * A condition in hash format represents the following SQL expression in general: + * `column1=value1 AND column2=value2 AND ...`. In case when a value is an array, + * an `IN` expression will be generated. And if a value is null, `IS NULL` will be used + * in the generated expression. Below are some examples: + * + * - `['type' => 1, 'status' => 2]` generates `(type = 1) AND (status = 2)`. + * - `['id' => [1, 2, 3], 'status' => 2]` generates `(id IN (1, 2, 3)) AND (status = 2)`. + * - `['status' => null] generates `status IS NULL`. + * + * A condition in operator format generates the SQL expression according to the specified operator, which + * can be one of the followings: + * + * - `and`: the operands should be concatenated together using `AND`. For example, + * `['and', 'id=1', 'id=2']` will generate `id=1 AND id=2`. If an operand is an array, + * it will be converted into a string using the rules described here. For example, + * `['and', 'type=1', ['or', 'id=1', 'id=2']]` will generate `type=1 AND (id=1 OR id=2)`. + * The method will NOT do any quoting or escaping. + * + * - `or`: similar to the `and` operator except that the operands are concatenated using `OR`. + * + * - `between`: operand 1 should be the column name, and operand 2 and 3 should be the + * starting and ending values of the range that the column is in. + * For example, `['between', 'id', 1, 10]` will generate `id BETWEEN 1 AND 10`. + * + * - `not between`: similar to `between` except the `BETWEEN` is replaced with `NOT BETWEEN` + * in the generated condition. + * + * - `in`: operand 1 should be a column or DB expression, and operand 2 be an array representing + * the range of the values that the column or DB expression should be in. For example, + * `['in', 'id', [1, 2, 3]]` will generate `id IN (1, 2, 3)`. + * The method will properly quote the column name and escape values in the range. + * + * - `not in`: similar to the `in` operator except that `IN` is replaced with `NOT IN` in the generated condition. + * + * - `like`: operand 1 should be a column or DB expression, and operand 2 be a string or an array representing + * the values that the column or DB expression should be like. + * For example, `['like', 'name', '%tester%']` will generate `name LIKE '%tester%'`. + * When the value range is given as an array, multiple `LIKE` predicates will be generated and concatenated + * using `AND`. For example, `['like', 'name', ['%test%', '%sample%']]` will generate + * `name LIKE '%test%' AND name LIKE '%sample%'`. + * The method will properly quote the column name and escape values in the range. + * + * - `or like`: similar to the `like` operator except that `OR` is used to concatenate the `LIKE` + * predicates when operand 2 is an array. + * + * - `not like`: similar to the `like` operator except that `LIKE` is replaced with `NOT LIKE` + * in the generated condition. + * + * - `or not like`: similar to the `not like` operator except that `OR` is used to concatenate + * the `NOT LIKE` predicates. + * + * @param array $condition the conditions that should be put in the WHERE part. + * @return static the query object itself + * @see andWhere() + * @see orWhere() + */ + public function where($condition); + + /** + * Adds an additional WHERE 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 WHERE condition. Please refer to [[where()]] + * on how to specify this parameter. + * @return static the query object itself + * @see where() + * @see orWhere() + */ + public function andWhere($condition); + + /** + * Adds an additional WHERE 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 WHERE condition. Please refer to [[where()]] + * on how to specify this parameter. + * @return static the query object itself + * @see where() + * @see andWhere() + */ + public function orWhere($condition); + + /** + * Sets the ORDER BY part of the query. + * @param string|array $columns the columns (and the directions) to be ordered by. + * Columns can be specified in either a string (e.g. "id ASC, name DESC") or an array + * (e.g. `['id' => SORT_ASC, 'name' => SORT_DESC]`). + * The method will automatically quote the column names unless a column contains some parenthesis + * (which means the column contains a DB expression). + * @return static the query object itself + * @see addOrderBy() + */ + public function orderBy($columns); + + /** + * Adds additional ORDER BY columns to the query. + * @param string|array $columns the columns (and the directions) to be ordered by. + * Columns can be specified in either a string (e.g. "id ASC, name DESC") or an array + * (e.g. `['id' => SORT_ASC, 'name' => SORT_DESC]`). + * The method will automatically quote the column names unless a column contains some parenthesis + * (which means the column contains a DB expression). + * @return static the query object itself + * @see orderBy() + */ + public function addOrderBy($columns); + + /** + * Sets the LIMIT part of the query. + * @param integer $limit the limit. Use null or negative value to disable limit. + * @return static the query object itself + */ + public function limit($limit); + + /** + * Sets the OFFSET part of the query. + * @param integer $offset the offset. Use null or negative value to disable offset. + * @return static the query object itself + */ + public function offset($offset); +} \ No newline at end of file diff --git a/framework/yii/db/QueryTrait.php b/framework/yii/db/QueryTrait.php index 92051ab..a963869 100644 --- a/framework/yii/db/QueryTrait.php +++ b/framework/yii/db/QueryTrait.php @@ -7,18 +7,6 @@ namespace yii\db; -// TODO where to put these constants? -/** - * Sort ascending - * @see orderBy - */ -const SORT_ASC = false; -/** - * Sort descending - * @see orderBy - */ -const SORT_DESC = true; - /** * The BaseQuery trait represents the minimum method set of a database Query. * @@ -53,8 +41,10 @@ trait QueryTrait /** * @var array how to sort the query results. This is used to construct the ORDER BY clause in a SQL statement. * The array keys are the columns to be sorted by, and the array values are the corresponding sort directions which - * can be either [[Query::SORT_ASC]] or [[Query::SORT_DESC]]. The array may also contain [[Expression]] objects. - * If that is the case, the expressions will be converted into strings without any change. + * can be either [SORT_ASC](http://php.net/manual/en/array.constants.php#constant.sort-asc) + * or [SORT_DESC](http://php.net/manual/en/array.constants.php#constant.sort-desc). + * The array may also contain [[Expression]] objects. If that is the case, the expressions + * will be converted into strings without any change. */ public $orderBy; /** @@ -86,90 +76,9 @@ trait QueryTrait } /** - * Executes the query and returns all results as an array. - * @return array the query results. If the query results in nothing, an empty array will be returned. - */ - abstract public function all(); - - /** - * Executes the query and returns a single row of result. - * @return array|boolean the first row (in terms of an array) of the query result. False is returned if the query - * results in nothing. - */ - abstract public function one(); - - /** - * Returns the number of records. - * @return integer number of records - */ - abstract public function count(); - - /** - * Returns a value indicating whether the query result contains any row of data. - * @return boolean whether the query result contains any row of data. - */ - abstract public function exists(); - - /** * Sets the WHERE part of the query. * - * The method requires a $condition parameter. - * - * The $condition parameter should be an array in one of the following two formats: - * - * - hash format: `['column1' => value1, 'column2' => value2, ...]` - * - operator format: `[operator, operand1, operand2, ...]` - * - * A condition in hash format represents the following SQL expression in general: - * `column1=value1 AND column2=value2 AND ...`. In case when a value is an array, - * an `IN` expression will be generated. And if a value is null, `IS NULL` will be used - * in the generated expression. Below are some examples: - * - * - `['type' => 1, 'status' => 2]` generates `(type = 1) AND (status = 2)`. - * - `['id' => [1, 2, 3], 'status' => 2]` generates `(id IN (1, 2, 3)) AND (status = 2)`. - * - `['status' => null] generates `status IS NULL`. - * - * A condition in operator format generates the SQL expression according to the specified operator, which - * can be one of the followings: - * - * - `and`: the operands should be concatenated together using `AND`. For example, - * `['and', 'id=1', 'id=2']` will generate `id=1 AND id=2`. If an operand is an array, - * it will be converted into a string using the rules described here. For example, - * `['and', 'type=1', ['or', 'id=1', 'id=2']]` will generate `type=1 AND (id=1 OR id=2)`. - * The method will NOT do any quoting or escaping. - * - * - `or`: similar to the `and` operator except that the operands are concatenated using `OR`. - * - * - `between`: operand 1 should be the column name, and operand 2 and 3 should be the - * starting and ending values of the range that the column is in. - * For example, `['between', 'id', 1, 10]` will generate `id BETWEEN 1 AND 10`. - * - * - `not between`: similar to `between` except the `BETWEEN` is replaced with `NOT BETWEEN` - * in the generated condition. - * - * - `in`: operand 1 should be a column or DB expression, and operand 2 be an array representing - * the range of the values that the column or DB expression should be in. For example, - * `['in', 'id', [1, 2, 3]]` will generate `id IN (1, 2, 3)`. - * The method will properly quote the column name and escape values in the range. - * - * - `not in`: similar to the `in` operator except that `IN` is replaced with `NOT IN` in the generated condition. - * - * - `like`: operand 1 should be a column or DB expression, and operand 2 be a string or an array representing - * the values that the column or DB expression should be like. - * For example, `['like', 'name', '%tester%']` will generate `name LIKE '%tester%'`. - * When the value range is given as an array, multiple `LIKE` predicates will be generated and concatenated - * using `AND`. For example, `['like', 'name', ['%test%', '%sample%']]` will generate - * `name LIKE '%test%' AND name LIKE '%sample%'`. - * The method will properly quote the column name and escape values in the range. - * - * - `or like`: similar to the `like` operator except that `OR` is used to concatenate the `LIKE` - * predicates when operand 2 is an array. - * - * - `not like`: similar to the `like` operator except that `LIKE` is replaced with `NOT LIKE` - * in the generated condition. - * - * - `or not like`: similar to the `not like` operator except that `OR` is used to concatenate - * the `NOT LIKE` predicates. + * See [[QueryInterface::where()]] for detailed documentation. * * @param array $condition the conditions that should be put in the WHERE part. * @return static the query object itself @@ -224,7 +133,7 @@ trait QueryTrait * Sets the ORDER BY part of the query. * @param string|array $columns the columns (and the directions) to be ordered by. * Columns can be specified in either a string (e.g. "id ASC, name DESC") or an array - * (e.g. `['id' => Query::SORT_ASC, 'name' => Query::SORT_DESC]`). + * (e.g. `['id' => SORT_ASC, 'name' => SORT_DESC]`). * The method will automatically quote the column names unless a column contains some parenthesis * (which means the column contains a DB expression). * @return static the query object itself @@ -240,7 +149,7 @@ trait QueryTrait * Adds additional ORDER BY columns to the query. * @param string|array $columns the columns (and the directions) to be ordered by. * Columns can be specified in either a string (e.g. "id ASC, name DESC") or an array - * (e.g. `['id' => Query::SORT_ASC, 'name' => Query::SORT_DESC]`). + * (e.g. `['id' => SORT_ASC, 'name' => SORT_DESC]`). * The method will automatically quote the column names unless a column contains some parenthesis * (which means the column contains a DB expression). * @return static the query object itself @@ -266,9 +175,9 @@ trait QueryTrait $result = []; foreach ($columns as $column) { if (preg_match('/^(.*?)\s+(asc|desc)$/i', $column, $matches)) { - $result[$matches[1]] = strcasecmp($matches[2], 'desc') ? self::SORT_ASC : self::SORT_DESC; + $result[$matches[1]] = strcasecmp($matches[2], 'desc') ? SORT_ASC : SORT_DESC; } else { - $result[$column] = self::SORT_ASC; + $result[$column] = SORT_ASC; } } return $result; From 9448c3d471de2e83f3db112e8d60ebc617bec12d Mon Sep 17 00:00:00 2001 From: Carsten Brandt Date: Wed, 13 Nov 2013 19:35:44 +0100 Subject: [PATCH 06/10] added unit tests for active dataprovider and fixed query tests --- framework/yii/ar/ActiveRelationTrait.php | 4 +- .../unit/framework/data/ActiveDataProviderTest.php | 74 ++++++++++++++++++++++ tests/unit/framework/db/ActiveRecordTest.php | 16 +++++ tests/unit/framework/db/QueryTest.php | 12 ++-- .../db/cubrid/CubridActiveDataProviderTest.php | 14 ++++ .../db/mssql/MssqlActiveDataProviderTest.php | 14 ++++ .../db/pgsql/PostgreSQLActiveDataProviderTest.php | 14 ++++ .../db/sqlite/SqliteActiveDataProviderTest.php | 14 ++++ 8 files changed, 154 insertions(+), 8 deletions(-) create mode 100644 tests/unit/framework/db/cubrid/CubridActiveDataProviderTest.php create mode 100644 tests/unit/framework/db/mssql/MssqlActiveDataProviderTest.php create mode 100644 tests/unit/framework/db/pgsql/PostgreSQLActiveDataProviderTest.php create mode 100644 tests/unit/framework/db/sqlite/SqliteActiveDataProviderTest.php diff --git a/framework/yii/ar/ActiveRelationTrait.php b/framework/yii/ar/ActiveRelationTrait.php index 350718d..5d49641 100644 --- a/framework/yii/ar/ActiveRelationTrait.php +++ b/framework/yii/ar/ActiveRelationTrait.php @@ -97,13 +97,13 @@ trait ActiveRelationTrait if ($this->via instanceof self) { // via pivot table - /** @var ActiveRelation $viaQuery */ + /** @var ActiveRelationTrait $viaQuery */ $viaQuery = $this->via; $viaModels = $viaQuery->findPivotRows($primaryModels); $this->filterByModels($viaModels); } elseif (is_array($this->via)) { // via relation - /** @var ActiveRelation $viaQuery */ + /** @var ActiveRelationTrait $viaQuery */ list($viaName, $viaQuery) = $this->via; $viaQuery->primaryModel = null; $viaModels = $viaQuery->findWith($viaName, $primaryModels); diff --git a/tests/unit/framework/data/ActiveDataProviderTest.php b/tests/unit/framework/data/ActiveDataProviderTest.php index 3b445e0..4e4ca77 100644 --- a/tests/unit/framework/data/ActiveDataProviderTest.php +++ b/tests/unit/framework/data/ActiveDataProviderTest.php @@ -10,6 +10,8 @@ namespace yiiunit\framework\data; use yii\data\ActiveDataProvider; use yii\db\Query; use yiiunit\data\ar\ActiveRecord; +use yiiunit\data\ar\Customer; +use yiiunit\data\ar\Item; use yiiunit\framework\db\DatabaseTestCase; use yiiunit\data\ar\Order; @@ -18,6 +20,7 @@ use yiiunit\data\ar\Order; * @since 2.0 * * @group data + * @group db */ class ActiveDataProviderTest extends DatabaseTestCase { @@ -35,6 +38,8 @@ class ActiveDataProviderTest extends DatabaseTestCase $orders = $provider->getModels(); $this->assertEquals(3, count($orders)); $this->assertTrue($orders[0] instanceof Order); + $this->assertTrue($orders[1] instanceof Order); + $this->assertTrue($orders[2] instanceof Order); $this->assertEquals([1, 2, 3], $provider->getKeys()); $provider = new ActiveDataProvider([ @@ -47,6 +52,75 @@ class ActiveDataProviderTest extends DatabaseTestCase $this->assertEquals(2, count($orders)); } + public function testActiveRelation() + { + /** @var Customer $customer */ + $customer = Customer::find(2); + $provider = new ActiveDataProvider([ + 'query' => $customer->getOrders(), + ]); + $orders = $provider->getModels(); + $this->assertEquals(2, count($orders)); + $this->assertTrue($orders[0] instanceof Order); + $this->assertTrue($orders[1] instanceof Order); + $this->assertEquals([2, 3], $provider->getKeys()); + + $provider = new ActiveDataProvider([ + 'query' => $customer->getOrders(), + 'pagination' => [ + 'pageSize' => 1, + ] + ]); + $orders = $provider->getModels(); + $this->assertEquals(1, count($orders)); + } + + public function testActiveRelationVia() + { + /** @var Order $order */ + $order = Order::find(2); + $provider = new ActiveDataProvider([ + 'query' => $order->getItems(), + ]); + $items = $provider->getModels(); + $this->assertEquals(3, count($items)); + $this->assertTrue($items[0] instanceof Item); + $this->assertTrue($items[1] instanceof Item); + $this->assertTrue($items[2] instanceof Item); + $this->assertEquals([3, 4, 5], $provider->getKeys()); + + $provider = new ActiveDataProvider([ + 'query' => $order->getItems(), + 'pagination' => [ + 'pageSize' => 2, + ] + ]); + $items = $provider->getModels(); + $this->assertEquals(2, count($items)); + } + + public function testActiveRelationViaTable() + { + /** @var Order $order */ + $order = Order::find(1); + $provider = new ActiveDataProvider([ + 'query' => $order->getBooks(), + ]); + $items = $provider->getModels(); + $this->assertEquals(2, count($items)); + $this->assertTrue($items[0] instanceof Item); + $this->assertTrue($items[1] instanceof Item); + + $provider = new ActiveDataProvider([ + 'query' => $order->getBooks(), + 'pagination' => [ + 'pageSize' => 1, + ] + ]); + $items = $provider->getModels(); + $this->assertEquals(1, count($items)); + } + public function testQuery() { $query = new Query; diff --git a/tests/unit/framework/db/ActiveRecordTest.php b/tests/unit/framework/db/ActiveRecordTest.php index d0d2f12..f96f2d3 100644 --- a/tests/unit/framework/db/ActiveRecordTest.php +++ b/tests/unit/framework/db/ActiveRecordTest.php @@ -119,10 +119,19 @@ class ActiveRecordTest extends DatabaseTestCase { /** @var Customer $customer */ $customer = Customer::find(2); + $this->assertFalse($customer->isRelationPopulated('orders')); $orders = $customer->orders; + $this->assertTrue($customer->isRelationPopulated('orders')); $this->assertEquals(2, count($orders)); + $this->assertEquals(1, count($customer->populatedRelations)); + /** @var Customer $customer */ + $customer = Customer::find(2); + $this->assertFalse($customer->isRelationPopulated('orders')); $orders = $customer->getOrders()->where('id=3')->all(); + $this->assertFalse($customer->isRelationPopulated('orders')); + $this->assertEquals(0, count($customer->populatedRelations)); + $this->assertEquals(1, count($orders)); $this->assertEquals(3, $orders[0]->id); } @@ -131,8 +140,15 @@ class ActiveRecordTest extends DatabaseTestCase { $customers = Customer::find()->with('orders')->all(); $this->assertEquals(3, count($customers)); + $this->assertTrue($customers[0]->isRelationPopulated('orders')); + $this->assertTrue($customers[1]->isRelationPopulated('orders')); $this->assertEquals(1, count($customers[0]->orders)); $this->assertEquals(2, count($customers[1]->orders)); + + $customer = Customer::find()->with('orders')->one(); + $this->assertTrue($customer->isRelationPopulated('orders')); + $this->assertEquals(1, count($customer->orders)); + $this->assertEquals(1, count($customer->populatedRelations)); } public function testFindLazyVia() diff --git a/tests/unit/framework/db/QueryTest.php b/tests/unit/framework/db/QueryTest.php index a48d719..77c1ac0 100644 --- a/tests/unit/framework/db/QueryTest.php +++ b/tests/unit/framework/db/QueryTest.php @@ -86,19 +86,19 @@ class QueryTest extends DatabaseTestCase { $query = new Query; $query->orderBy('team'); - $this->assertEquals(['team' => false], $query->orderBy); + $this->assertEquals(['team' => SORT_ASC], $query->orderBy); $query->addOrderBy('company'); - $this->assertEquals(['team' => false, 'company' => false], $query->orderBy); + $this->assertEquals(['team' => SORT_ASC, 'company' => SORT_ASC], $query->orderBy); $query->addOrderBy('age'); - $this->assertEquals(['team' => false, 'company' => false, 'age' => false], $query->orderBy); + $this->assertEquals(['team' => SORT_ASC, 'company' => SORT_ASC, 'age' => SORT_ASC], $query->orderBy); - $query->addOrderBy(['age' => true]); - $this->assertEquals(['team' => false, 'company' => false, 'age' => true], $query->orderBy); + $query->addOrderBy(['age' => SORT_DESC]); + $this->assertEquals(['team' => SORT_ASC, 'company' => SORT_ASC, 'age' => SORT_DESC], $query->orderBy); $query->addOrderBy('age ASC, company DESC'); - $this->assertEquals(['team' => false, 'company' => true, 'age' => false], $query->orderBy); + $this->assertEquals(['team' => SORT_ASC, 'company' => SORT_DESC, 'age' => SORT_ASC], $query->orderBy); } public function testLimitOffset() diff --git a/tests/unit/framework/db/cubrid/CubridActiveDataProviderTest.php b/tests/unit/framework/db/cubrid/CubridActiveDataProviderTest.php new file mode 100644 index 0000000..191442a --- /dev/null +++ b/tests/unit/framework/db/cubrid/CubridActiveDataProviderTest.php @@ -0,0 +1,14 @@ + Date: Wed, 13 Nov 2013 19:47:35 +0100 Subject: [PATCH 07/10] dataprovider: reset orderBy when counting strict sql dbms like postgres would fail otherwise --- framework/yii/data/ActiveDataProvider.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/framework/yii/data/ActiveDataProvider.php b/framework/yii/data/ActiveDataProvider.php index 3766692..a8bb18f 100644 --- a/framework/yii/data/ActiveDataProvider.php +++ b/framework/yii/data/ActiveDataProvider.php @@ -158,7 +158,7 @@ class ActiveDataProvider extends BaseDataProvider throw new InvalidConfigException('The "query" property must be an instance of a class that implements the QueryInterface e.g. yii\db\Query or its subclasses.'); } $query = clone $this->query; - return (int) $query->limit(-1)->offset(-1)->count($this->db); + return (int) $query->limit(-1)->offset(-1)->orderBy([])->count($this->db); } /** From 51faa62ecab71cfb5458e45ead7cb0fa3051efc4 Mon Sep 17 00:00:00 2001 From: Carsten Brandt Date: Wed, 13 Nov 2013 19:52:14 +0100 Subject: [PATCH 08/10] updated interface and trait php-doc in yii\ar [ci skip] --- framework/yii/ar/ActiveQueryInterface.php | 2 +- framework/yii/ar/ActiveQueryTrait.php | 25 +------------------------ framework/yii/ar/ActiveRelationInterface.php | 2 ++ framework/yii/ar/ActiveRelationTrait.php | 11 +---------- 4 files changed, 5 insertions(+), 35 deletions(-) diff --git a/framework/yii/ar/ActiveQueryInterface.php b/framework/yii/ar/ActiveQueryInterface.php index e2bb780..ddcb027 100644 --- a/framework/yii/ar/ActiveQueryInterface.php +++ b/framework/yii/ar/ActiveQueryInterface.php @@ -9,7 +9,7 @@ namespace yii\ar; use yii\db\QueryInterface; /** - * ActiveQueryInterface defines the common interface to be implemented by active record relation classes. + * ActiveQueryInterface defines the common interface to be implemented by active record query classes. * * A class implementing this interface should also use [[ActiveQueryTrait]]. * diff --git a/framework/yii/ar/ActiveQueryTrait.php b/framework/yii/ar/ActiveQueryTrait.php index 8c8d6c0..e45f72a 100644 --- a/framework/yii/ar/ActiveQueryTrait.php +++ b/framework/yii/ar/ActiveQueryTrait.php @@ -10,30 +10,7 @@ namespace yii\ar; use yii\db\ActiveRecord; /** - * ActiveQueryTrait implements the common set of methods that are used by DB queries associated with an Active Record class. - * - * 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. - * - [[scalar()]]: returns the value of the first column in the first row of 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: - * - * ~~~ - * $customers = Customer::find()->with('orders')->asArray()->all(); - * ~~~ + * ActiveQueryTrait implements the common methods and properties for active record query classes. * * @author Qiang Xue * @author Carsten Brandt diff --git a/framework/yii/ar/ActiveRelationInterface.php b/framework/yii/ar/ActiveRelationInterface.php index 70b723a..1d58a7d 100644 --- a/framework/yii/ar/ActiveRelationInterface.php +++ b/framework/yii/ar/ActiveRelationInterface.php @@ -10,6 +10,8 @@ namespace yii\ar; /** * ActiveRelationInterface defines the common interface to be implemented by active record relation classes. * + * A class implementing this interface should also use [[ActiveRelationTrait]]. + * * @author Qiang Xue * @author Carsten Brandt * @since 2.0 diff --git a/framework/yii/ar/ActiveRelationTrait.php b/framework/yii/ar/ActiveRelationTrait.php index 5d49641..6b617bc 100644 --- a/framework/yii/ar/ActiveRelationTrait.php +++ b/framework/yii/ar/ActiveRelationTrait.php @@ -11,16 +11,7 @@ use yii\base\InvalidConfigException; use yii\db\ActiveRecord; /** - * ActiveRelation represents a relation between two Active Record classes. - * - * ActiveRelation 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 ActiveRelation 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. + * ActiveRelationTrait implements the common methods and properties for active record relation classes. * * @author Qiang Xue * @author Carsten Brandt From 1acdbb741c1f2b6a0a3c9329c85512d3e62c41e8 Mon Sep 17 00:00:00 2001 From: Carsten Brandt Date: Wed, 13 Nov 2013 19:57:32 +0100 Subject: [PATCH 09/10] moved indexBy() trait -> interface + removed findWith from interface --- framework/yii/ar/ActiveQueryInterface.php | 19 +++++++++++++++++++ framework/yii/ar/ActiveQueryTrait.php | 22 ---------------------- framework/yii/ar/ActiveRelationInterface.php | 9 --------- 3 files changed, 19 insertions(+), 31 deletions(-) diff --git a/framework/yii/ar/ActiveQueryInterface.php b/framework/yii/ar/ActiveQueryInterface.php index ddcb027..7a1efa1 100644 --- a/framework/yii/ar/ActiveQueryInterface.php +++ b/framework/yii/ar/ActiveQueryInterface.php @@ -27,6 +27,25 @@ interface ActiveQueryInterface extends QueryInterface public function asArray($value = true); /** + * Sets the [[indexBy]] property. + * @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 + * } + * ~~~ + * + * @return static the query object itself + */ + public function indexBy($column); + + /** * 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 diff --git a/framework/yii/ar/ActiveQueryTrait.php b/framework/yii/ar/ActiveQueryTrait.php index e45f72a..98e7870 100644 --- a/framework/yii/ar/ActiveQueryTrait.php +++ b/framework/yii/ar/ActiveQueryTrait.php @@ -103,28 +103,6 @@ trait ActiveQueryTrait } /** - * Sets the [[indexBy]] property. - * @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 - * } - * ~~~ - * - * @return static the query object itself - */ - public function indexBy($column) - { - return parent::indexBy($column); - } - - /** * Converts found rows into model instances * @param array $rows * @return array|ActiveRecord[] diff --git a/framework/yii/ar/ActiveRelationInterface.php b/framework/yii/ar/ActiveRelationInterface.php index 1d58a7d..706d394 100644 --- a/framework/yii/ar/ActiveRelationInterface.php +++ b/framework/yii/ar/ActiveRelationInterface.php @@ -26,13 +26,4 @@ interface ActiveRelationInterface extends ActiveQueryInterface * @return static the relation object itself. */ public function via($relationName, $callable = null); - - /** - * Finds the related records and populates them into the primary models. - * This method is internally used by [[ActiveQuery]]. Do not call it directly. - * @param string $name the relation name - * @param array $primaryModels primary models - * @return array the related models - */ - public function findWith($name, &$primaryModels); } From 8250cfbb0f1b3b87ba1ed901c8d38bf1b92bdcfb Mon Sep 17 00:00:00 2001 From: Carsten Brandt Date: Thu, 14 Nov 2013 08:06:57 +0100 Subject: [PATCH 10/10] reverted change of count() signature --- framework/yii/ar/ActiveRelationTrait.php | 2 +- framework/yii/data/ActiveDataProvider.php | 2 +- framework/yii/db/Query.php | 6 +++--- framework/yii/db/QueryInterface.php | 3 ++- 4 files changed, 7 insertions(+), 6 deletions(-) diff --git a/framework/yii/ar/ActiveRelationTrait.php b/framework/yii/ar/ActiveRelationTrait.php index 6b617bc..d87b22d 100644 --- a/framework/yii/ar/ActiveRelationTrait.php +++ b/framework/yii/ar/ActiveRelationTrait.php @@ -33,7 +33,7 @@ trait ActiveRelationTrait * @var array the columns of the primary and foreign tables that establish the relation. * The array keys must be columns of the table for this relation, and the array values * must be the corresponding columns from the primary table. - * Do not prefix or quote the column names as they will be done automatically by Yii. + * Do not prefix or quote the column names as this will be done automatically by Yii. */ public $link; /** diff --git a/framework/yii/data/ActiveDataProvider.php b/framework/yii/data/ActiveDataProvider.php index a8bb18f..0943be4 100644 --- a/framework/yii/data/ActiveDataProvider.php +++ b/framework/yii/data/ActiveDataProvider.php @@ -158,7 +158,7 @@ class ActiveDataProvider extends BaseDataProvider throw new InvalidConfigException('The "query" property must be an instance of a class that implements the QueryInterface e.g. yii\db\Query or its subclasses.'); } $query = clone $this->query; - return (int) $query->limit(-1)->offset(-1)->orderBy([])->count($this->db); + return (int) $query->limit(-1)->offset(-1)->orderBy([])->count('*', $this->db); } /** diff --git a/framework/yii/db/Query.php b/framework/yii/db/Query.php index 0ae5f44..50ed105 100644 --- a/framework/yii/db/Query.php +++ b/framework/yii/db/Query.php @@ -179,13 +179,13 @@ class Query extends Component implements QueryInterface /** * Returns the number of records. - * @param Connection $db the database connection used to generate the SQL statement. - * If this parameter is not given (or null), the `db` application component will be used. * @param string $q the COUNT expression. Defaults to '*'. * 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 (or null), the `db` application component will be used. * @return integer number of records */ - public function count($db = null, $q = '*') + public function count($q = '*', $db = null) { $this->select = ["COUNT($q)"]; return $this->createCommand($db)->queryScalar(); diff --git a/framework/yii/db/QueryInterface.php b/framework/yii/db/QueryInterface.php index d13fa2b..f3cc312 100644 --- a/framework/yii/db/QueryInterface.php +++ b/framework/yii/db/QueryInterface.php @@ -44,11 +44,12 @@ interface QueryInterface /** * Returns the number of records. + * @param string $q the COUNT expression. Defaults to '*'. * @param Connection $db the database connection used to execute the query. * If this parameter is not given, the `db` application component will be used. * @return integer number of records */ - public function count($db = null); + public function count($q = '*', $db = null); /** * Returns a value indicating whether the query result contains any row of data.