Browse Source

refactored query builder.

finished Sort.
tags/2.0.0-beta
Qiang Xue 12 years ago
parent
commit
06feccff8b
  1. 119
      framework/db/Query.php
  2. 106
      framework/db/QueryBuilder.php
  3. 3
      framework/web/Pagination.php
  4. 318
      framework/web/Sort.php
  5. 24
      tests/unit/framework/db/QueryTest.php

119
framework/db/Query.php

@ -35,9 +35,19 @@ namespace yii\db;
class Query extends \yii\base\Component class Query extends \yii\base\Component
{ {
/** /**
* @var string|array the columns being selected. This refers to the SELECT clause in a SQL * Sort ascending
* statement. It can be either a string (e.g. `'id, name'`) or an array (e.g. `array('id', 'name')`). * @see orderBy
* If not set, if means all columns. */
const SORT_ASC = false;
/**
* Sort ascending
* @see orderBy
*/
const SORT_DESC = true;
/**
* @var array the columns being selected. For example, `array('id', 'name')`.
* This is used to construct the SELECT clause in a SQL statement. If not set, if means selecting all columns.
* @see select() * @see select()
*/ */
public $select; public $select;
@ -52,8 +62,8 @@ class Query extends \yii\base\Component
*/ */
public $distinct; public $distinct;
/** /**
* @var string|array the table(s) to be selected from. This refers to the FROM clause in a SQL statement. * @var array the table(s) to be selected from. For example, `array('tbl_user', 'tbl_post')`.
* It can be either a string (e.g. `'tbl_user, tbl_post'`) or an array (e.g. `array('tbl_user', 'tbl_post')`). * This is used to construct the FROM clause in a SQL statement.
* @see from() * @see from()
*/ */
public $from; public $from;
@ -73,20 +83,33 @@ class Query extends \yii\base\Component
*/ */
public $offset; public $offset;
/** /**
* @var string|array how to sort the query results. This refers to the ORDER BY clause in a SQL statement. * @var array how to sort the query results. This is used to construct the ORDER BY clause in a SQL statement.
* It can be either a string (e.g. `'id ASC, name DESC'`) or an array (e.g. `array('id ASC', 'name DESC')`). * 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; public $orderBy;
/** /**
* @var string|array how to group the query results. This refers to the GROUP BY clause in a SQL statement. * @var array how to group the query results. For example, `array('company', 'department')`.
* It can be either a string (e.g. `'company, department'`) or an array (e.g. `array('company', 'department')`). * This is used to construct the GROUP BY clause in a SQL statement.
*/ */
public $groupBy; public $groupBy;
/** /**
* @var string|array how to join with other tables. This refers to the JOIN clause in a SQL statement. * @var array how to join with other tables. Each array element represents the specification
* It can be either a string (e.g. `'LEFT JOIN tbl_user ON tbl_user.id=author_id'`) or an array (e.g. * of one join which has the following structure:
* `array('LEFT JOIN tbl_user ON tbl_user.id=author_id', 'LEFT JOIN tbl_team ON tbl_team.id=team_id')`). *
* @see join() * ~~~
* array($joinType, $tableName, $joinCondition)
* ~~~
*
* For example,
*
* ~~~
* array(
* array('INNER JOIN', 'tbl_user', 'tbl_user.id = author_id'),
* array('LEFT JOIN', 'tbl_team', 'tbl_team.id = team_id'),
* )
* ~~~
*/ */
public $join; public $join;
/** /**
@ -95,9 +118,8 @@ class Query extends \yii\base\Component
*/ */
public $having; public $having;
/** /**
* @var string|Query[] the UNION clause(s) in a SQL statement. This can be either a string * @var array this is used to construct the UNION clause(s) in a SQL statement.
* representing a single UNION clause or an array representing multiple UNION clauses. * Each array element can be either a string or a [[Query]] object representing a sub-query.
* Each union clause can be a string or a `Query` object which refers to the SQL statement.
*/ */
public $union; public $union;
/** /**
@ -134,6 +156,9 @@ class Query extends \yii\base\Component
*/ */
public function select($columns, $option = null) public function select($columns, $option = null)
{ {
if (!is_array($columns)) {
$columns = preg_split('/\s*,\s*/', trim($columns), -1, PREG_SPLIT_NO_EMPTY);
}
$this->select = $columns; $this->select = $columns;
$this->selectOption = $option; $this->selectOption = $option;
return $this; return $this;
@ -161,6 +186,9 @@ class Query extends \yii\base\Component
*/ */
public function from($tables) public function from($tables)
{ {
if (!is_array($tables)) {
$tables = preg_split('/\s*,\s*/', trim($tables), -1, PREG_SPLIT_NO_EMPTY);
}
$this->from = $tables; $this->from = $tables;
return $this; return $this;
} }
@ -360,10 +388,13 @@ class Query extends \yii\base\Component
* The method will automatically quote the column names unless a column contains some parenthesis * The method will automatically quote the column names unless a column contains some parenthesis
* (which means the column contains a DB expression). * (which means the column contains a DB expression).
* @return Query the query object itself * @return Query the query object itself
* @see addGroup() * @see addGroupBy()
*/ */
public function groupBy($columns) public function groupBy($columns)
{ {
if (!is_array($columns)) {
$columns = preg_split('/\s*,\s*/', trim($columns), -1, PREG_SPLIT_NO_EMPTY);
}
$this->groupBy = $columns; $this->groupBy = $columns;
return $this; return $this;
} }
@ -375,19 +406,16 @@ class Query extends \yii\base\Component
* The method will automatically quote the column names unless a column contains some parenthesis * The method will automatically quote the column names unless a column contains some parenthesis
* (which means the column contains a DB expression). * (which means the column contains a DB expression).
* @return Query the query object itself * @return Query the query object itself
* @see group() * @see groupBy()
*/ */
public function addGroup($columns) public function addGroupBy($columns)
{ {
if (empty($this->groupBy)) { if (!is_array($columns)) {
$columns = preg_split('/\s*,\s*/', trim($columns), -1, PREG_SPLIT_NO_EMPTY);
}
if ($this->groupBy === null) {
$this->groupBy = $columns; $this->groupBy = $columns;
} else { } else {
if (!is_array($this->groupBy)) {
$this->groupBy = preg_split('/\s*,\s*/', trim($this->groupBy), -1, PREG_SPLIT_NO_EMPTY);
}
if (!is_array($columns)) {
$columns = preg_split('/\s*,\s*/', trim($columns), -1, PREG_SPLIT_NO_EMPTY);
}
$this->groupBy = array_merge($this->groupBy, $columns); $this->groupBy = array_merge($this->groupBy, $columns);
} }
return $this; return $this;
@ -454,43 +482,58 @@ class Query extends \yii\base\Component
/** /**
* Sets the ORDER BY part of the query. * Sets the ORDER BY part of the query.
* @param string|array $columns the columns (and the directions) to be ordered by. * @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. array('id ASC', 'name DESC')). * Columns can be specified in either a string (e.g. "id ASC, name DESC") or an array
* (e.g. `array('id' => Query::SORT_ASC ASC, 'name' => Query::SORT_DESC)`).
* The method will automatically quote the column names unless a column contains some parenthesis * The method will automatically quote the column names unless a column contains some parenthesis
* (which means the column contains a DB expression). * (which means the column contains a DB expression).
* @return Query the query object itself * @return Query the query object itself
* @see addOrder() * @see addOrderBy()
*/ */
public function orderBy($columns) public function orderBy($columns)
{ {
$this->orderBy = $columns; $this->orderBy = $this->normalizeOrderBy($columns);
return $this; return $this;
} }
/** /**
* Adds additional ORDER BY columns to the query. * Adds additional ORDER BY columns to the query.
* @param string|array $columns the columns (and the directions) to be ordered by. * @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. array('id ASC', 'name DESC')). * Columns can be specified in either a string (e.g. "id ASC, name DESC") or an array
* (e.g. `array('id' => Query::SORT_ASC ASC, 'name' => Query::SORT_DESC)`).
* The method will automatically quote the column names unless a column contains some parenthesis * The method will automatically quote the column names unless a column contains some parenthesis
* (which means the column contains a DB expression). * (which means the column contains a DB expression).
* @return Query the query object itself * @return Query the query object itself
* @see order() * @see orderBy()
*/ */
public function addOrderBy($columns) public function addOrderBy($columns)
{ {
if (empty($this->orderBy)) { $columns = $this->normalizeOrderBy($columns);
if ($this->orderBy === null) {
$this->orderBy = $columns; $this->orderBy = $columns;
} else { } else {
if (!is_array($this->orderBy)) {
$this->orderBy = preg_split('/\s*,\s*/', trim($this->orderBy), -1, PREG_SPLIT_NO_EMPTY);
}
if (!is_array($columns)) {
$columns = preg_split('/\s*,\s*/', trim($columns), -1, PREG_SPLIT_NO_EMPTY);
}
$this->orderBy = array_merge($this->orderBy, $columns); $this->orderBy = array_merge($this->orderBy, $columns);
} }
return $this; 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 = array();
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. * Sets the LIMIT part of the query.
* @param integer $limit the limit * @param integer $limit the limit

106
framework/db/QueryBuilder.php

@ -60,10 +60,10 @@ class QueryBuilder extends \yii\base\Object
$this->buildFrom($query->from), $this->buildFrom($query->from),
$this->buildJoin($query->join), $this->buildJoin($query->join),
$this->buildWhere($query->where), $this->buildWhere($query->where),
$this->buildGroup($query->groupBy), $this->buildGroupBy($query->groupBy),
$this->buildHaving($query->having), $this->buildHaving($query->having),
$this->buildUnion($query->union), $this->buildUnion($query->union),
$this->buildOrder($query->orderBy), $this->buildOrderBy($query->orderBy),
$this->buildLimit($query->limit, $query->offset), $this->buildLimit($query->limit, $query->offset),
); );
return implode($this->separator, array_filter($clauses)); return implode($this->separator, array_filter($clauses));
@ -673,7 +673,7 @@ class QueryBuilder extends \yii\base\Object
} }
/** /**
* @param string|array $columns * @param array $columns
* @param boolean $distinct * @param boolean $distinct
* @param string $selectOption * @param string $selectOption
* @return string the SELECT clause built from [[query]]. * @return string the SELECT clause built from [[query]].
@ -689,13 +689,6 @@ class QueryBuilder extends \yii\base\Object
return $select . ' *'; return $select . ' *';
} }
if (!is_array($columns)) {
if (strpos($columns, '(') !== false) {
return $select . ' ' . $columns;
} else {
$columns = preg_split('/\s*,\s*/', trim($columns), -1, PREG_SPLIT_NO_EMPTY);
}
}
foreach ($columns as $i => $column) { foreach ($columns as $i => $column) {
if (is_object($column)) { if (is_object($column)) {
$columns[$i] = (string)$column; $columns[$i] = (string)$column;
@ -716,7 +709,7 @@ class QueryBuilder extends \yii\base\Object
} }
/** /**
* @param string|array $tables * @param array $tables
* @return string the FROM clause built from [[query]]. * @return string the FROM clause built from [[query]].
*/ */
public function buildFrom($tables) public function buildFrom($tables)
@ -725,13 +718,6 @@ class QueryBuilder extends \yii\base\Object
return ''; return '';
} }
if (!is_array($tables)) {
if (strpos($tables, '(') !== false) {
return 'FROM ' . $tables;
} else {
$tables = preg_split('/\s*,\s*/', trim($tables), -1, PREG_SPLIT_NO_EMPTY);
}
}
foreach ($tables as $i => $table) { foreach ($tables as $i => $table) {
if (strpos($table, '(') === false) { if (strpos($table, '(') === false) {
if (preg_match('/^(.*?)(?i:\s+as\s+|\s+)(.*)$/i', $table, $matches)) { // with alias if (preg_match('/^(.*?)(?i:\s+as\s+|\s+)(.*)$/i', $table, $matches)) { // with alias
@ -752,37 +738,36 @@ class QueryBuilder extends \yii\base\Object
/** /**
* @param string|array $joins * @param string|array $joins
* @return string the JOIN clause built from [[query]]. * @return string the JOIN clause built from [[query]].
* @throws Exception if the $joins parameter is not in proper format
*/ */
public function buildJoin($joins) public function buildJoin($joins)
{ {
if (empty($joins)) { if (empty($joins)) {
return ''; return '';
} }
if (is_string($joins)) {
return $joins;
}
foreach ($joins as $i => $join) { foreach ($joins as $i => $join) {
if (is_array($join)) { // 0:join type, 1:table name, 2:on-condition if (is_object($join)) {
if (isset($join[0], $join[1])) { $joins[$i] = (string)$join;
$table = $join[1]; } elseif (is_array($join) && isset($join[0], $join[1])) {
if (strpos($table, '(') === false) { // 0:join type, 1:table name, 2:on-condition
if (preg_match('/^(.*?)(?i:\s+as\s+|\s+)(.*)$/', $table, $matches)) { // with alias $table = $join[1];
$table = $this->db->quoteTableName($matches[1]) . ' ' . $this->db->quoteTableName($matches[2]); if (strpos($table, '(') === false) {
} else { if (preg_match('/^(.*?)(?i:\s+as\s+|\s+)(.*)$/', $table, $matches)) { // with alias
$table = $this->db->quoteTableName($table); $table = $this->db->quoteTableName($matches[1]) . ' ' . $this->db->quoteTableName($matches[2]);
} } else {
$table = $this->db->quoteTableName($table);
} }
$joins[$i] = $join[0] . ' ' . $table; }
if (isset($join[2])) { $joins[$i] = $join[0] . ' ' . $table;
$condition = $this->buildCondition($join[2]); if (isset($join[2])) {
if ($condition !== '') { $condition = $this->buildCondition($join[2]);
$joins[$i] .= ' ON ' . $this->buildCondition($join[2]); if ($condition !== '') {
} $joins[$i] .= ' ON ' . $this->buildCondition($join[2]);
} }
} else {
throw new Exception('A join clause must be specified as an array of at least two elements.');
} }
} else {
throw new Exception('A join clause must be specified as an array of join type, join table, and optionally join condition.');
} }
} }
@ -800,16 +785,12 @@ class QueryBuilder extends \yii\base\Object
} }
/** /**
* @param string|array $columns * @param array $columns
* @return string the GROUP BY clause * @return string the GROUP BY clause
*/ */
public function buildGroup($columns) public function buildGroupBy($columns)
{ {
if (empty($columns)) { return empty($columns) ? '' : 'GROUP BY ' . $this->buildColumns($columns);
return '';
} else {
return 'GROUP BY ' . $this->buildColumns($columns);
}
} }
/** /**
@ -823,36 +804,24 @@ class QueryBuilder extends \yii\base\Object
} }
/** /**
* @param string|array $columns * @param array $columns
* @return string the ORDER BY clause built from [[query]]. * @return string the ORDER BY clause built from [[query]].
*/ */
public function buildOrder($columns) public function buildOrderBy($columns)
{ {
if (empty($columns)) { if (empty($columns)) {
return ''; return '';
} }
if (!is_array($columns)) { $orders = array();
if (strpos($columns, '(') !== false) { foreach ($columns as $name => $direction) {
return 'ORDER BY ' . $columns; if (is_object($direction)) {
$orders[] = (string)$direction;
} else { } else {
$columns = preg_split('/\s*,\s*/', trim($columns), -1, PREG_SPLIT_NO_EMPTY); $orders[] = $this->db->quoteColumnName($name) . ($direction === Query::SORT_DESC ? ' DESC' : '');
}
}
foreach ($columns as $i => $column) {
if (is_object($column)) {
$columns[$i] = (string)$column;
} elseif (strpos($column, '(') === false) {
if (preg_match('/^(.*?)\s+(asc|desc)$/i', $column, $matches)) {
$columns[$i] = $this->db->quoteColumnName($matches[1]) . ' ' . $matches[2];
} else {
$columns[$i] = $this->db->quoteColumnName($column);
}
} }
} }
if (is_array($columns)) {
$columns = implode(', ', $columns); return 'ORDER BY ' . implode(', ', $orders);
}
return 'ORDER BY ' . $columns;
} }
/** /**
@ -873,7 +842,7 @@ class QueryBuilder extends \yii\base\Object
} }
/** /**
* @param string|array $unions * @param array $unions
* @return string the UNION clause built from [[query]]. * @return string the UNION clause built from [[query]].
*/ */
public function buildUnion($unions) public function buildUnion($unions)
@ -881,9 +850,6 @@ class QueryBuilder extends \yii\base\Object
if (empty($unions)) { if (empty($unions)) {
return ''; return '';
} }
if (!is_array($unions)) {
$unions = array($unions);
}
foreach ($unions as $i => $union) { foreach ($unions as $i => $union) {
if ($union instanceof Query) { if ($union instanceof Query) {
$unions[$i] = $this->build($union); $unions[$i] = $this->build($union);

3
framework/web/Pagination.php

@ -65,7 +65,8 @@ use Yii;
class Pagination extends \yii\base\Object class Pagination extends \yii\base\Object
{ {
/** /**
* @var string name of the GET variable storing the current page index. Defaults to 'page'. * @var string name of the parameter storing the current page index. Defaults to 'page'.
* @see params
*/ */
public $pageVar = 'page'; public $pageVar = 'page';
/** /**

318
framework/web/Sort.php

@ -8,7 +8,6 @@
namespace yii\web; namespace yii\web;
use Yii; use Yii;
use yii\util\StringHelper;
use yii\util\Html; use yii\util\Html;
/** /**
@ -18,38 +17,24 @@ use yii\util\Html;
* we can use Sort to represent the sorting information and generate * we can use Sort to represent the sorting information and generate
* appropriate hyperlinks that can lead to sort actions. * appropriate hyperlinks that can lead to sort actions.
* *
* Sort is designed to be used together with {@link CActiveRecord}. * A typical usage example is as follows,
* When creating a Sort instance, you need to specify {@link modelClass}.
* You can use Sort to generate hyperlinks by calling {@link link}.
* You can also use Sort to modify a {@link CDbCriteria} instance by calling {@link applyOrder} so that
* it can cause the query results to be sorted according to the specified
* attributes.
*
* In order to prevent SQL injection attacks, Sort ensures that only valid model attributes
* can be sorted. This is determined based on {@link modelClass} and {@link attributes}.
* When {@link attributes} is not set, all attributes belonging to {@link modelClass}
* can be sorted. When {@link attributes} is set, only those attributes declared in the property
* can be sorted.
*
* By configuring {@link attributes}, one can perform more complex sorts that may
* consist of things like compound attributes (e.g. sort based on the combination of
* first name and last name of users).
*
* The property {@link attributes} should be an array of key-value pairs, where the keys
* represent the attribute names, while the values represent the virtual attribute definitions.
* For more details, please check the documentation about {@link attributes}.
*
* * Controller action:
* *
* ~~~ * ~~~
* function actionIndex() * function actionIndex()
* { * {
* $sort = new Sort(array( * $sort = new Sort(array(
* 'attributes' => Article::attributes(), * 'attributes' => array(
* 'age',
* 'name' => array(
* 'asc' => array('last_name', 'first_name'),
* 'desc' => array('last_name' => true, 'first_name' => true),
* ),
* ),
* )); * ));
*
* $models = Article::find() * $models = Article::find()
* ->where(array('status' => 1)) * ->where(array('status' => 1))
* ->orderBy($sort->orderBy) * ->orderBy($sort->orders)
* ->all(); * ->all();
* *
* $this->render('index', array( * $this->render('index', array(
@ -62,21 +47,23 @@ use yii\util\Html;
* View: * View:
* *
* ~~~ * ~~~
* // display links leading to sort actions
* echo $sort->link('name', 'Name') . ' | ' . $sort->link('age', 'Age');
*
* foreach($models as $model) { * foreach($models as $model) {
* // display $model here * // display $model here
* } * }
*
* // display pagination
* $this->widget('yii\web\widgets\LinkPager', array(
* 'pages' => $pages,
* ));
* ~~~ * ~~~
* *
* @property string $orderBy The order-by columns represented by this sort object. * In the above, we declare two [[attributes]] that support sorting: name and age.
* This can be put in the ORDER BY clause of a SQL statement. * We pass the sort information to the Article query so that the query results are
* @property array $directions Sort directions indexed by attribute names. * sorted by the orders specified by the Sort object. In the view, we show two hyperlinks
* The sort direction. Can be either Sort::SORT_ASC for ascending order or * that can lead to pages with the data sorted by the corresponding attributes.
* Sort::SORT_DESC for descending order. *
* @property array $orders Sort directions indexed by column names. The sort direction
* can be either [[Sort::ASC]] for ascending order or [[Sort::DESC]] for descending order.
* @property array $attributeOrders Sort directions indexed by attribute names. The sort
* direction can be either [[Sort::ASC]] for ascending order or [[Sort::DESC]] for descending order.
* *
* @author Qiang Xue <qiang.xue@gmail.com> * @author Qiang Xue <qiang.xue@gmail.com>
* @since 2.0 * @since 2.0
@ -86,12 +73,12 @@ class Sort extends \yii\base\Object
/** /**
* Sort ascending * Sort ascending
*/ */
const SORT_ASC = false; const ASC = false;
/** /**
* Sort descending * Sort descending
*/ */
const SORT_DESC = true; const DESC = true;
/** /**
* @var boolean whether the sorting can be applied to multiple attributes simultaneously. * @var boolean whether the sorting can be applied to multiple attributes simultaneously.
@ -100,111 +87,66 @@ class Sort extends \yii\base\Object
public $enableMultiSort = false; public $enableMultiSort = false;
/** /**
* @var array list of attributes that are allowed to be sorted. * @var array list of attributes that are allowed to be sorted. Its syntax can be
* For example, array('user_id','create_time') would specify that only 'user_id' * described using the following example:
* and 'create_time' of the model {@link modelClass} can be sorted.
* By default, this property is an empty array, which means all attributes in
* {@link modelClass} are allowed to be sorted.
*
* This property can also be used to specify complex sorting. To do so,
* a virtual attribute can be declared in terms of a key-value pair in the array.
* The key refers to the name of the virtual attribute that may appear in the sort request,
* while the value specifies the definition of the virtual attribute.
* *
* In the simple case, a key-value pair can be like <code>'user'=>'user_id'</code> * ~~~
* where 'user' is the name of the virtual attribute while 'user_id' means the virtual * array(
* attribute is the 'user_id' attribute in the {@link modelClass}. * 'age',
* * 'user' => array(
* A more flexible way is to specify the key-value pair as * 'asc' => array('first_name' => Sort::ASC, 'last_name' => Sort::ASC),
* <pre> * 'desc' => array('first_name' => Sort::DESC, 'last_name' => Sort::DESC),
* 'user'=>array( * 'default' => 'desc',
* 'asc'=>'first_name, last_name', * ),
* 'desc'=>'first_name DESC, last_name DESC',
* 'label'=>'Name'
* )
* </pre>
* where 'user' is the name of the virtual attribute that specifies the full name of user
* (a compound attribute consisting of first name and last name of user). In this case,
* we have to use an array to define the virtual attribute with three elements: 'asc',
* 'desc' and 'label'.
*
* The above approach can also be used to declare virtual attributes that consist of relational
* attributes. For example,
* <pre>
* 'price'=>array(
* 'asc'=>'item.price',
* 'desc'=>'item.price DESC',
* 'label'=>'Item Price'
* ) * )
* </pre> * ~~~
* *
* Note, the attribute name should not contain '-' or '.' characters because * In the above, two attributes are declared: "age" and "user". The "age" attribute is
* they are used as {@link separators}. * a simple attribute which is equivalent to the following:
* *
* Starting from version 1.1.3, an additional option named 'default' can be used in the virtual attribute * ~~~
* declaration. This option specifies whether an attribute should be sorted in ascending or descending * 'age' => array(
* order upon user clicking the corresponding sort hyperlink if it is not currently sorted. The valid * 'asc' => array('age' => Sort::ASC),
* option values include 'asc' (default) and 'desc'. For example, * 'desc' => array('age' => Sort::DESC),
* <pre>
* 'price'=>array(
* 'asc'=>'item.price',
* 'desc'=>'item.price DESC',
* 'label'=>'Item Price',
* 'default'=>'desc',
* ) * )
* </pre> * ~~~
* *
* Also starting from version 1.1.3, you can include a star ('*') element in this property so that * The "user" attribute is a composite attribute:
* all model attributes are available for sorting, in addition to those virtual attributes. For example, *
* <pre> * - The "user" key represents the attribute name which will appear in the URLs leading
* 'attributes'=>array( * to sort actions. Attribute names cannot contain characters listed in [[separators]].
* 'price'=>array( * - The "asc" and "desc" elements specify how to sort by the attribute in ascending
* 'asc'=>'item.price', * and descending orders, respectively. Their values represent the actual columns and
* 'desc'=>'item.price DESC', * the directions by which the data should be sorted by.
* 'label'=>'Item Price', * - And the "default" element specifies if the attribute is not sorted currently,
* 'default'=>'desc', * in which direction it should be sorted (the default value is ascending order).
* ),
* '*',
* )
* </pre>
* Note that when a name appears as both a model attribute and a virtual attribute, the position of
* the star element in the array determines which one takes precedence. In particular, if the star
* element is the first element in the array, the model attribute takes precedence; and if the star
* element is the last one, the virtual attribute takes precedence.
*/ */
public $attributes = array(); public $attributes = array();
/** /**
* @var string the name of the GET parameter that specifies which attributes to be sorted * @var string the name of the parameter that specifies which attributes to be sorted
* in which direction. Defaults to 'sort'. * in which direction. Defaults to 'sort'.
* @see params
*/ */
public $sortVar = 'sort'; public $sortVar = 'sort';
/** /**
* @var string the tag appeared in the GET parameter that indicates the attribute should be sorted * @var string the tag appeared in the [[sortVar]] parameter that indicates the attribute should be sorted
* in descending order. Defaults to 'desc'. * in descending order. Defaults to 'desc'.
*/ */
public $descTag = 'desc'; public $descTag = 'desc';
/** /**
* @var mixed the default order that should be applied to the query criteria when * @var array the order that should be used when the current request does not specify any order.
* the current request does not specify any sort. For example, 'name, create_time DESC' or * The array keys are attribute names and the array values are the corresponding sort directions. For example,
* 'UPPER(name)'.
* *
* Starting from version 1.1.3, you can also specify the default order using an array. * ~~~
* The array keys could be attribute names or virtual attribute names as declared in {@link attributes}, * array(
* and the array values indicate whether the sorting of the corresponding attributes should * 'name' => Sort::ASC,
* be in descending order. For example, * 'create_time' => Sort::DESC,
* <pre>
* 'defaultOrder'=>array(
* 'price'=>Sort::SORT_DESC,
* ) * )
* </pre> * ~~~
* `SORT_DESC` and `SORT_ASC` are available since 1.1.10. In earlier Yii versions you should use
* `true` and `false` respectively.
* *
* Please note when using array to specify the default order, the corresponding attributes * @see attributeOrders
* will be put into {@link directions} and thus affect how the sort links are rendered
* (e.g. an arrow may be displayed next to the currently active sort link).
*/ */
public $defaultOrder; public $defaults;
/** /**
* @var string the route of the controller action for displaying the sorted contents. * @var string the route of the controller action for displaying the sorted contents.
* If not set, it means using the currently requested route. * If not set, it means using the currently requested route.
@ -218,61 +160,52 @@ class Sort extends \yii\base\Object
*/ */
public $separators = array('-', '.'); public $separators = array('-', '.');
/** /**
* @var array parameters (name=>value) that should be used to obtain the current sort directions * @var array parameters (name => value) that should be used to obtain the current sort directions
* and to create new sort URLs. If not set, $_GET will be used instead. * and to create new sort URLs. If not set, $_GET will be used instead.
* *
* The array element indexed by [[sortVar]] is considered to be the current sort directions. * The array element indexed by [[sortVar]] is considered to be the current sort directions.
* If the element does not exist, the [[defaultOrder]] will be used. * If the element does not exist, the [[defaults|default order]] will be used.
*
* @see sortVar
* @see defaults
*/ */
public $params; public $params;
private $_directions;
/** /**
* @return string the order-by columns represented by this sort object. * Returns the columns and their corresponding sort directions.
* This can be put in the ORDER BY clause of a SQL statement. * @return array the columns (keys) and their corresponding sort directions (values).
* This can be passed to [[\yii\db\Query::orderBy()]] to construct a DB query.
*/ */
public function getOrderBy() public function getOrders()
{ {
$directions = $this->getDirections(); $attributeOrders = $this->getAttributeOrders();
if (empty($directions)) { $orders = array();
return is_string($this->defaultOrder) ? $this->defaultOrder : ''; foreach ($attributeOrders as $attribute => $direction) {
} else { $definition = $this->getAttribute($attribute);
$orders = array(); $columns = $definition[$direction === self::ASC ? 'asc' : 'desc'];
foreach ($directions as $attribute => $descending) { foreach ($columns as $name => $dir) {
$definition = $this->getDefinition($attribute); $orders[$name] = $dir;
if ($descending) {
$orders[] = isset($definition['desc']) ? $definition['desc'] : $attribute . ' DESC';
} else {
$orders[] = isset($definition['asc']) ? $definition['asc'] : $attribute;
}
} }
return implode(', ', $orders);
} }
return $orders;
} }
/** /**
* Generates a hyperlink that can be clicked to cause sorting. * Generates a hyperlink that links to the sort action to sort by the specified attribute.
* @param string $attribute the attribute name. This must be the actual attribute name, not alias. * Based on the sort direction, the CSS class of the generated hyperlink will be appended
* If it is an attribute of a related AR object, the name should be prefixed with * with "asc" or "desc".
* the relation name (e.g. 'author.name', where 'author' is the relation name). * @param string $attribute the attribute name by which the data should be sorted by.
* @param string $label the link label. If null, the label will be determined according * @param string $label the link label. Note that the label will not be HTML-encoded.
* to the attribute (see {@link resolveLabel}).
* @param array $htmlOptions additional HTML attributes for the hyperlink tag * @param array $htmlOptions additional HTML attributes for the hyperlink tag
* @return string the generated hyperlink * @return string the generated hyperlink
*/ */
public function link($attribute, $label = null, $htmlOptions = array()) public function link($attribute, $label, $htmlOptions = array())
{ {
if (($definition = $this->getDefinition($attribute)) === false) { if (($definition = $this->getAttribute($attribute)) === false) {
return false; return $label;
}
if ($label === null) {
$label = isset($definition['label']) ? $definition['label'] : StringHelper::camel2words($attribute);
} }
if (($direction = $this->getDirection($attribute)) !== null) { if (($direction = $this->getAttributeOrder($attribute)) !== null) {
$class = $direction ? 'desc' : 'asc'; $class = $direction ? 'desc' : 'asc';
if (isset($htmlOptions['class'])) { if (isset($htmlOptions['class'])) {
$htmlOptions['class'] .= ' ' . $class; $htmlOptions['class'] .= ' ' . $class;
@ -286,17 +219,19 @@ class Sort extends \yii\base\Object
return Html::link($label, $url, $htmlOptions); return Html::link($label, $url, $htmlOptions);
} }
private $_attributeOrders;
/** /**
* Returns the currently requested sort information. * Returns the currently requested sort information.
* @param boolean $recalculate whether to recalculate the sort directions * @param boolean $recalculate whether to recalculate the sort directions
* @return array sort directions indexed by attribute names. * @return array sort directions indexed by attribute names.
* Sort direction can be either Sort::SORT_ASC for ascending order or * Sort direction can be either [[Sort::ASC]] for ascending order or
* Sort::SORT_DESC for descending order. * [[Sort::DESC]] for descending order.
*/ */
public function getDirections($recalculate = false) public function getAttributeOrders($recalculate = false)
{ {
if ($this->_directions === null || $recalculate) { if ($this->_attributeOrders === null || $recalculate) {
$this->_directions = array(); $this->_attributeOrders = array();
$params = $this->params === null ? $_GET : $this->params; $params = $this->params === null ? $_GET : $this->params;
if (isset($params[$this->sortVar]) && is_scalar($params[$this->sortVar])) { if (isset($params[$this->sortVar]) && is_scalar($params[$this->sortVar])) {
$attributes = explode($this->separators[0], $params[$this->sortVar]); $attributes = explode($this->separators[0], $params[$this->sortVar]);
@ -308,49 +243,50 @@ class Sort extends \yii\base\Object
} }
} }
if (($this->getDefinition($attribute)) !== false) { if (($this->getAttribute($attribute)) !== false) {
$this->_directions[$attribute] = $descending; $this->_attributeOrders[$attribute] = $descending;
if (!$this->enableMultiSort) { if (!$this->enableMultiSort) {
return $this->_directions; return $this->_attributeOrders;
} }
} }
} }
} }
if ($this->_directions === array() && is_array($this->defaultOrder)) { if ($this->_attributeOrders === array() && is_array($this->defaults)) {
$this->_directions = $this->defaultOrder; $this->_attributeOrders = $this->defaults;
} }
} }
return $this->_directions; return $this->_attributeOrders;
} }
/** /**
* Returns the sort direction of the specified attribute in the current request. * Returns the sort direction of the specified attribute in the current request.
* @param string $attribute the attribute name * @param string $attribute the attribute name
* @return boolean|null Sort direction of the attribute. Can be either Sort::SORT_ASC * @return boolean|null Sort direction of the attribute. Can be either [[Sort::ASC]]
* for ascending order or Sort::SORT_DESC for descending order. Value is null * for ascending order or [[Sort::DESC]] for descending order. Null is returned
* if the attribute does not need to be sorted. * if the attribute is invalid or does not need to be sorted.
*/ */
public function getDirection($attribute) public function getAttributeOrder($attribute)
{ {
$this->getDirections(); $this->getAttributeOrders();
return isset($this->_directions[$attribute]) ? $this->_directions[$attribute] : null; return isset($this->_attributeOrders[$attribute]) ? $this->_attributeOrders[$attribute] : null;
} }
/** /**
* Creates a URL for sorting the data by the specified attribute. * Creates a URL for sorting the data by the specified attribute.
* This method will consider the current sorting status given by [[directions]]. * This method will consider the current sorting status given by [[attributeOrders]].
* For example, if the current page already sorts the data by the specified attribute in ascending order, * For example, if the current page already sorts the data by the specified attribute in ascending order,
* then the URL created will lead to a page that sorts the data by the specified attribute in descending order. * then the URL created will lead to a page that sorts the data by the specified attribute in descending order.
* @param string $attribute the attribute name * @param string $attribute the attribute name
* @return string|boolean the URL for sorting. False if the attribute is invalid. * @return string|boolean the URL for sorting. False if the attribute is invalid.
* @see attributeOrders
* @see params * @see params
*/ */
public function createUrl($attribute) public function createUrl($attribute)
{ {
if (($definition = $this->getDefinition($attribute)) === false) { if (($definition = $this->getAttribute($attribute)) === false) {
return false; return false;
} }
$directions = $this->getDirections(); $directions = $this->getAttributeOrders();
if (isset($directions[$attribute])) { if (isset($directions[$attribute])) {
$descending = !$directions[$attribute]; $descending = !$directions[$attribute];
unset($directions[$attribute]); unset($directions[$attribute]);
@ -378,28 +314,20 @@ class Sort extends \yii\base\Object
} }
/** /**
* Returns the real definition of an attribute given its name. * Returns the attribute definition of the specified name.
* * @param string $name the attribute name
* The resolution is based on {@link attributes} and {@link CActiveRecord::attributeNames}. * @return array|boolean the sort definition (column names => sort directions).
* <ul> * False is returned if the attribute cannot be sorted.
* <li>When {@link attributes} is an empty array, if the name refers to an attribute of {@link modelClass}, * @see attributes
* then the name is returned back.</li>
* <li>When {@link attributes} is not empty, if the name refers to an attribute declared in {@link attributes},
* then the corresponding virtual attribute definition is returned. Starting from version 1.1.3, if {@link attributes}
* contains a star ('*') element, the name will also be used to match against all model attributes.</li>
* <li>In all other cases, false is returned, meaning the name does not refer to a valid attribute.</li>
* </ul>
* @param string $attribute the attribute name that the user requests to sort on
* @return mixed the attribute name or the virtual attribute definition. False if the attribute cannot be sorted.
*/ */
public function getDefinition($attribute) public function getAttribute($name)
{ {
if (isset($this->attributes[$attribute])) { if (isset($this->attributes[$name])) {
return $this->attributes[$attribute]; return $this->attributes[$name];
} elseif (in_array($attribute, $this->attributes, true)) { } elseif (in_array($name, $this->attributes, true)) {
return array( return array(
'asc' => $attribute, 'asc' => array($name => self::ASC),
'desc' => "$attribute DESC", 'desc' => array($name => self::DESC),
); );
} else { } else {
return false; return false;

24
tests/unit/framework/db/QueryTest.php

@ -14,13 +14,13 @@ class QueryTest extends \yiiunit\MysqlTestCase
// default // default
$query = new Query; $query = new Query;
$query->select('*'); $query->select('*');
$this->assertEquals('*', $query->select); $this->assertEquals(array('*'), $query->select);
$this->assertNull($query->distinct); $this->assertNull($query->distinct);
$this->assertEquals(null, $query->selectOption); $this->assertEquals(null, $query->selectOption);
$query = new Query; $query = new Query;
$query->select('id, name', 'something')->distinct(true); $query->select('id, name', 'something')->distinct(true);
$this->assertEquals('id, name', $query->select); $this->assertEquals(array('id','name'), $query->select);
$this->assertTrue($query->distinct); $this->assertTrue($query->distinct);
$this->assertEquals('something', $query->selectOption); $this->assertEquals('something', $query->selectOption);
} }
@ -29,7 +29,7 @@ class QueryTest extends \yiiunit\MysqlTestCase
{ {
$query = new Query; $query = new Query;
$query->from('tbl_user'); $query->from('tbl_user');
$this->assertEquals('tbl_user', $query->from); $this->assertEquals(array('tbl_user'), $query->from);
} }
function testWhere() function testWhere()
@ -57,12 +57,12 @@ class QueryTest extends \yiiunit\MysqlTestCase
{ {
$query = new Query; $query = new Query;
$query->groupBy('team'); $query->groupBy('team');
$this->assertEquals('team', $query->groupBy); $this->assertEquals(array('team'), $query->groupBy);
$query->addGroup('company'); $query->addGroupBy('company');
$this->assertEquals(array('team', 'company'), $query->groupBy); $this->assertEquals(array('team', 'company'), $query->groupBy);
$query->addGroup('age'); $query->addGroupBy('age');
$this->assertEquals(array('team', 'company', 'age'), $query->groupBy); $this->assertEquals(array('team', 'company', 'age'), $query->groupBy);
} }
@ -86,13 +86,19 @@ class QueryTest extends \yiiunit\MysqlTestCase
{ {
$query = new Query; $query = new Query;
$query->orderBy('team'); $query->orderBy('team');
$this->assertEquals('team', $query->orderBy); $this->assertEquals(array('team' => false), $query->orderBy);
$query->addOrderBy('company'); $query->addOrderBy('company');
$this->assertEquals(array('team', 'company'), $query->orderBy); $this->assertEquals(array('team' => false, 'company' => false), $query->orderBy);
$query->addOrderBy('age'); $query->addOrderBy('age');
$this->assertEquals(array('team', 'company', 'age'), $query->orderBy); $this->assertEquals(array('team' => false, 'company' => false, 'age' => false), $query->orderBy);
$query->addOrderBy(array('age' => true));
$this->assertEquals(array('team' => false, 'company' => false, 'age' => true), $query->orderBy);
$query->addOrderBy('age ASC, company DESC');
$this->assertEquals(array('team' => false, 'company' => true, 'age' => false), $query->orderBy);
} }
function testLimitOffset() function testLimitOffset()

Loading…
Cancel
Save