Sort.php 14.9 KB
Newer Older
Qiang Xue committed
1 2 3
<?php
/**
 * @link http://www.yiiframework.com/
Qiang Xue committed
4
 * @copyright Copyright (c) 2008 Yii Software LLC
Qiang Xue committed
5 6 7
 * @license http://www.yiiframework.com/license/
 */

8
namespace yii\data;
Qiang Xue committed
9

Qiang Xue committed
10
use Yii;
Qiang Xue committed
11
use yii\base\InvalidConfigException;
12
use yii\base\Object;
Qiang Xue committed
13
use yii\helpers\Html;
Qiang Xue committed
14
use yii\helpers\Inflector;
15
use yii\web\Request;
Qiang Xue committed
16

Qiang Xue committed
17
/**
Qiang Xue committed
18
 * Sort represents information relevant to sorting.
Qiang Xue committed
19 20
 *
 * When data needs to be sorted according to one or several attributes,
Qiang Xue committed
21
 * we can use Sort to represent the sorting information and generate
Qiang Xue committed
22 23
 * appropriate hyperlinks that can lead to sort actions.
 *
Qiang Xue committed
24
 * A typical usage example is as follows,
Qiang Xue committed
25
 *
Carsten Brandt committed
26
 * ```php
Qiang Xue committed
27 28
 * function actionIndex()
 * {
Alexander Makarov committed
29 30
 *     $sort = new Sort([
 *         'attributes' => [
Qiang Xue committed
31
 *             'age',
Alexander Makarov committed
32
 *             'name' => [
33 34 35
 *                 'asc' => ['first_name' => SORT_ASC, 'last_name' => SORT_ASC],
 *                 'desc' => ['first_name' => SORT_DESC, 'last_name' => SORT_DESC],
 *                 'default' => SORT_DESC,
Qiang Xue committed
36
 *                 'label' => 'Name',
Alexander Makarov committed
37 38 39
 *             ],
 *         ],
 *     ]);
Qiang Xue committed
40
 *
Qiang Xue committed
41
 *     $models = Article::find()
Alexander Makarov committed
42
 *         ->where(['status' => 1])
Qiang Xue committed
43
 *         ->orderBy($sort->orders)
Qiang Xue committed
44 45
 *         ->all();
 *
Alexander Makarov committed
46
 *     return $this->render('index', [
Qiang Xue committed
47 48
 *          'models' => $models,
 *          'sort' => $sort,
Alexander Makarov committed
49
 *     ]);
Qiang Xue committed
50
 * }
Carsten Brandt committed
51
 * ```
Qiang Xue committed
52 53 54
 *
 * View:
 *
Carsten Brandt committed
55
 * ```php
Qiang Xue committed
56
 * // display links leading to sort actions
Qiang Xue committed
57
 * echo $sort->link('name') . ' | ' . $sort->link('age');
Qiang Xue committed
58
 *
resurtm committed
59
 * foreach ($models as $model) {
Qiang Xue committed
60 61
 *     // display $model here
 * }
Carsten Brandt committed
62
 * ```
Qiang Xue committed
63
 *
Qiang Xue committed
64 65 66 67 68
 * In the above, we declare two [[attributes]] that support sorting: name and age.
 * We pass the sort information to the Article query so that the query results are
 * sorted by the orders specified by the Sort object. In the view, we show two hyperlinks
 * that can lead to pages with the data sorted by the corresponding attributes.
 *
69
 * @property array $attributeOrders Sort directions indexed by attribute names. Sort direction can be either
70
 * `SORT_ASC` for ascending order or `SORT_DESC` for descending order. This property is read-only.
71 72
 * @property array $orders The columns (keys) and their corresponding sort directions (values). This can be
 * passed to [[\yii\db\Query::orderBy()]] to construct a DB query. This property is read-only.
73
 *
Qiang Xue committed
74
 * @author Qiang Xue <qiang.xue@gmail.com>
Qiang Xue committed
75
 * @since 2.0
Qiang Xue committed
76
 */
77
class Sort extends Object
Qiang Xue committed
78
{
79 80 81 82 83 84 85 86 87
    /**
     * @var boolean whether the sorting can be applied to multiple attributes simultaneously.
     * Defaults to false, which means each time the data can only be sorted by one attribute.
     */
    public $enableMultiSort = false;
    /**
     * @var array list of attributes that are allowed to be sorted. Its syntax can be
     * described using the following example:
     *
Carsten Brandt committed
88
     * ```php
89 90 91 92 93 94 95 96 97
     * [
     *     'age',
     *     'name' => [
     *         'asc' => ['first_name' => SORT_ASC, 'last_name' => SORT_ASC],
     *         'desc' => ['first_name' => SORT_DESC, 'last_name' => SORT_DESC],
     *         'default' => SORT_DESC,
     *         'label' => 'Name',
     *     ],
     * ]
Carsten Brandt committed
98
     * ```
99 100 101 102
     *
     * In the above, two attributes are declared: "age" and "name". The "age" attribute is
     * a simple attribute which is equivalent to the following:
     *
Carsten Brandt committed
103
     * ```php
104 105 106 107 108 109
     * 'age' => [
     *     'asc' => ['age' => SORT_ASC],
     *     'desc' => ['age' => SORT_DESC],
     *     'default' => SORT_ASC,
     *     'label' => Inflector::camel2words('age'),
     * ]
Carsten Brandt committed
110
     * ```
111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138
     *
     * The "name" attribute is a composite attribute:
     *
     * - The "name" key represents the attribute name which will appear in the URLs leading
     *   to sort actions.
     * - The "asc" and "desc" elements specify how to sort by the attribute in ascending
     *   and descending orders, respectively. Their values represent the actual columns and
     *   the directions by which the data should be sorted by.
     * - The "default" element specifies by which direction the attribute should be sorted
     *   if it is not currently sorted (the default value is ascending order).
     * - The "label" element specifies what label should be used when calling [[link()]] to create
     *   a sort link. If not set, [[Inflector::camel2words()]] will be called to get a label.
     *   Note that it will not be HTML-encoded.
     *
     * Note that if the Sort object is already created, you can only use the full format
     * to configure every attribute. Each attribute must include these elements: `asc` and `desc`.
     */
    public $attributes = [];
    /**
     * @var string the name of the parameter that specifies which attributes to be sorted
     * in which direction. Defaults to 'sort'.
     * @see params
     */
    public $sortParam = 'sort';
    /**
     * @var array the order that should be used when the current request does not specify any order.
     * The array keys are attribute names and the array values are the corresponding sort directions. For example,
     *
Carsten Brandt committed
139
     * ```php
140 141 142 143
     * [
     *     'name' => SORT_ASC,
     *     'created_at' => SORT_DESC,
     * ]
Carsten Brandt committed
144
     * ```
145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175
     *
     * @see attributeOrders
     */
    public $defaultOrder;
    /**
     * @var string the route of the controller action for displaying the sorted contents.
     * If not set, it means using the currently requested route.
     */
    public $route;
    /**
     * @var string the character used to separate different attributes that need to be sorted by.
     */
    public $separator = ',';
    /**
     * @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.
     *
     * In order to add hash to all links use `array_merge($_GET, ['#' => 'my-hash'])`.
     *
     * The array element indexed by [[sortParam]] is considered to be the current sort directions.
     * If the element does not exist, the [[defaultOrder|default order]] will be used.
     *
     * @see sortParam
     * @see defaultOrder
     */
    public $params;
    /**
     * @var \yii\web\UrlManager the URL manager used for creating sort URLs. If not set,
     * the "urlManager" application component will be used.
     */
    public $urlManager;
Qiang Xue committed
176

Carsten Brandt committed
177

178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200
    /**
     * Normalizes the [[attributes]] property.
     */
    public function init()
    {
        $attributes = [];
        foreach ($this->attributes as $name => $attribute) {
            if (!is_array($attribute)) {
                $attributes[$attribute] = [
                    'asc' => [$attribute => SORT_ASC],
                    'desc' => [$attribute => SORT_DESC],
                ];
            } elseif (!isset($attribute['asc'], $attribute['desc'])) {
                $attributes[$name] = array_merge([
                    'asc' => [$name => SORT_ASC],
                    'desc' => [$name => SORT_DESC],
                ], $attribute);
            } else {
                $attributes[$name] = $attribute;
            }
        }
        $this->attributes = $attributes;
    }
Qiang Xue committed
201

202 203
    /**
     * Returns the columns and their corresponding sort directions.
204 205 206
     * @param boolean $recalculate whether to recalculate the sort directions
     * @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.
207 208 209 210 211 212 213 214 215 216 217 218
     */
    public function getOrders($recalculate = false)
    {
        $attributeOrders = $this->getAttributeOrders($recalculate);
        $orders = [];
        foreach ($attributeOrders as $attribute => $direction) {
            $definition = $this->attributes[$attribute];
            $columns = $definition[$direction === SORT_ASC ? 'asc' : 'desc'];
            foreach ($columns as $name => $dir) {
                $orders[$name] = $dir;
            }
        }
Qiang Xue committed
219

220 221
        return $orders;
    }
Qiang Xue committed
222

223 224 225 226
    /**
     * @var array the currently requested sort order as computed by [[getAttributeOrders]].
     */
    private $_attributeOrders;
Qiang Xue committed
227

228 229
    /**
     * Returns the currently requested sort information.
230 231 232 233
     * @param boolean $recalculate whether to recalculate the sort directions
     * @return array sort directions indexed by attribute names.
     * Sort direction can be either `SORT_ASC` for ascending order or
     * `SORT_DESC` for descending order.
234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250
     */
    public function getAttributeOrders($recalculate = false)
    {
        if ($this->_attributeOrders === null || $recalculate) {
            $this->_attributeOrders = [];
            if (($params = $this->params) === null) {
                $request = Yii::$app->getRequest();
                $params = $request instanceof Request ? $request->getQueryParams() : [];
            }
            if (isset($params[$this->sortParam]) && is_scalar($params[$this->sortParam])) {
                $attributes = explode($this->separator, $params[$this->sortParam]);
                foreach ($attributes as $attribute) {
                    $descending = false;
                    if (strncmp($attribute, '-', 1) === 0) {
                        $descending = true;
                        $attribute = substr($attribute, 1);
                    }
Qiang Xue committed
251

252 253 254 255 256 257 258 259 260 261 262 263
                    if (isset($this->attributes[$attribute])) {
                        $this->_attributeOrders[$attribute] = $descending ? SORT_DESC : SORT_ASC;
                        if (!$this->enableMultiSort) {
                            return $this->_attributeOrders;
                        }
                    }
                }
            }
            if (empty($this->_attributeOrders) && is_array($this->defaultOrder)) {
                $this->_attributeOrders = $this->defaultOrder;
            }
        }
Qiang Xue committed
264

265 266
        return $this->_attributeOrders;
    }
Qiang Xue committed
267

268 269
    /**
     * Returns the sort direction of the specified attribute in the current request.
270
     * @param string $attribute the attribute name
271
     * @return boolean|null Sort direction of the attribute. Can be either `SORT_ASC`
272 273
     * for ascending order or `SORT_DESC` for descending order. Null is returned
     * if the attribute is invalid or does not need to be sorted.
274 275 276 277
     */
    public function getAttributeOrder($attribute)
    {
        $orders = $this->getAttributeOrders();
278

279 280
        return isset($orders[$attribute]) ? $orders[$attribute] : null;
    }
Qiang Xue committed
281

282 283 284 285
    /**
     * Generates a hyperlink that links to the sort action to sort by the specified attribute.
     * Based on the sort direction, the CSS class of the generated hyperlink will be appended
     * with "asc" or "desc".
286 287 288 289 290 291 292
     * @param string $attribute the attribute name by which the data should be sorted by.
     * @param array $options additional HTML attributes for the hyperlink tag.
     * There is one special attribute `label` which will be used as the label of the hyperlink.
     * If this is not set, the label defined in [[attributes]] will be used.
     * If no label is defined, [[\yii\helpers\Inflector::camel2words()]] will be called to get a label.
     * Note that it will not be HTML-encoded.
     * @return string the generated hyperlink
293 294 295 296 297 298 299 300 301 302 303 304
     * @throws InvalidConfigException if the attribute is unknown
     */
    public function link($attribute, $options = [])
    {
        if (($direction = $this->getAttributeOrder($attribute)) !== null) {
            $class = $direction === SORT_DESC ? 'desc' : 'asc';
            if (isset($options['class'])) {
                $options['class'] .= ' ' . $class;
            } else {
                $options['class'] = $class;
            }
        }
Qiang Xue committed
305

306 307
        $url = $this->createUrl($attribute);
        $options['data-sort'] = $this->createSortParam($attribute);
Qiang Xue committed
308

309 310 311 312 313 314 315 316 317 318
        if (isset($options['label'])) {
            $label = $options['label'];
            unset($options['label']);
        } else {
            if (isset($this->attributes[$attribute]['label'])) {
                $label = $this->attributes[$attribute]['label'];
            } else {
                $label = Inflector::camel2words($attribute);
            }
        }
Qiang Xue committed
319

320 321
        return Html::a($label, $url, $options);
    }
Qiang Xue committed
322

323 324 325 326 327
    /**
     * Creates a URL for sorting the data by the specified attribute.
     * 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,
     * then the URL created will lead to a page that sorts the data by the specified attribute in descending order.
328 329 330
     * @param string $attribute the attribute name
     * @param boolean $absolute whether to create an absolute URL. Defaults to `false`.
     * @return string the URL for sorting. False if the attribute is invalid.
331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354
     * @throws InvalidConfigException if the attribute is unknown
     * @see attributeOrders
     * @see params
     */
    public function createUrl($attribute, $absolute = false)
    {
        if (($params = $this->params) === null) {
            $request = Yii::$app->getRequest();
            $params = $request instanceof Request ? $request->getQueryParams() : [];
        }
        $params[$this->sortParam] = $this->createSortParam($attribute);
        $params[0] = $this->route === null ? Yii::$app->controller->getRoute() : $this->route;
        $urlManager = $this->urlManager === null ? Yii::$app->getUrlManager() : $this->urlManager;
        if ($absolute) {
            return $urlManager->createAbsoluteUrl($params);
        } else {
            return $urlManager->createUrl($params);
        }
    }

    /**
     * Creates the sort variable for the specified attribute.
     * The newly created sort variable can be used to create a URL that will lead to
     * sorting by the specified attribute.
355 356
     * @param string $attribute the attribute name
     * @return string the value of the sort variable
357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388
     * @throws InvalidConfigException if the specified attribute is not defined in [[attributes]]
     */
    public function createSortParam($attribute)
    {
        if (!isset($this->attributes[$attribute])) {
            throw new InvalidConfigException("Unknown attribute: $attribute");
        }
        $definition = $this->attributes[$attribute];
        $directions = $this->getAttributeOrders();
        if (isset($directions[$attribute])) {
            $direction = $directions[$attribute] === SORT_DESC ? SORT_ASC : SORT_DESC;
            unset($directions[$attribute]);
        } else {
            $direction = isset($definition['default']) ? $definition['default'] : SORT_ASC;
        }

        if ($this->enableMultiSort) {
            $directions = array_merge([$attribute => $direction], $directions);
        } else {
            $directions = [$attribute => $direction];
        }

        $sorts = [];
        foreach ($directions as $attribute => $direction) {
            $sorts[] = $direction === SORT_DESC ? '-' . $attribute : $attribute;
        }

        return implode($this->separator, $sorts);
    }

    /**
     * Returns a value indicating whether the sort definition supports sorting by the named attribute.
389
     * @param string $name the attribute name
390 391 392 393 394 395
     * @return boolean whether the sort definition supports sorting by the named attribute.
     */
    public function hasAttribute($name)
    {
        return isset($this->attributes[$name]);
    }
Zander Baldwin committed
396
}