1 | <?php |
||
2 | /** |
||
3 | * @link http://www.yiiframework.com/ |
||
4 | * @copyright Copyright (c) 2008 Yii Software LLC |
||
5 | * @license http://www.yiiframework.com/license/ |
||
6 | */ |
||
7 | |||
8 | namespace yii\data; |
||
9 | |||
10 | use Yii; |
||
11 | use yii\base\BaseObject; |
||
12 | use yii\base\InvalidConfigException; |
||
13 | use yii\helpers\Html; |
||
14 | use yii\helpers\Inflector; |
||
15 | use yii\web\Request; |
||
16 | |||
17 | /** |
||
18 | * Sort represents information relevant to sorting. |
||
19 | * |
||
20 | * When data needs to be sorted according to one or several attributes, |
||
21 | * we can use Sort to represent the sorting information and generate |
||
22 | * appropriate hyperlinks that can lead to sort actions. |
||
23 | * |
||
24 | * A typical usage example is as follows, |
||
25 | * |
||
26 | * ```php |
||
27 | * public function actionIndex() |
||
28 | * { |
||
29 | * $sort = new Sort([ |
||
30 | * 'attributes' => [ |
||
31 | * 'age', |
||
32 | * 'name' => [ |
||
33 | * 'asc' => ['first_name' => SORT_ASC, 'last_name' => SORT_ASC], |
||
34 | * 'desc' => ['first_name' => SORT_DESC, 'last_name' => SORT_DESC], |
||
35 | * 'default' => SORT_DESC, |
||
36 | * 'label' => 'Name', |
||
37 | * ], |
||
38 | * ], |
||
39 | * ]); |
||
40 | * |
||
41 | * $models = Article::find() |
||
42 | * ->where(['status' => 1]) |
||
43 | * ->orderBy($sort->orders) |
||
44 | * ->all(); |
||
45 | * |
||
46 | * return $this->render('index', [ |
||
47 | * 'models' => $models, |
||
48 | * 'sort' => $sort, |
||
49 | * ]); |
||
50 | * } |
||
51 | * ``` |
||
52 | * |
||
53 | * View: |
||
54 | * |
||
55 | * ```php |
||
56 | * // display links leading to sort actions |
||
57 | * echo $sort->link('name') . ' | ' . $sort->link('age'); |
||
58 | * |
||
59 | * foreach ($models as $model) { |
||
60 | * // display $model here |
||
61 | * } |
||
62 | * ``` |
||
63 | * |
||
64 | * In the above, we declare two [[attributes]] that support sorting: `name` and `age`. |
||
65 | * We pass the sort information to the Article query so that the query results are |
||
66 | * sorted by the orders specified by the Sort object. In the view, we show two hyperlinks |
||
67 | * that can lead to pages with the data sorted by the corresponding attributes. |
||
68 | * |
||
69 | * For more details and usage information on Sort, see the [guide article on sorting](guide:output-sorting). |
||
70 | * |
||
71 | * @property array $attributeOrders Sort directions indexed by attribute names. Sort direction can be either |
||
72 | * `SORT_ASC` for ascending order or `SORT_DESC` for descending order. Note that the type of this property |
||
73 | * differs in getter and setter. See [[getAttributeOrders()]] and [[setAttributeOrders()]] for details. |
||
74 | * @property-read array $orders The columns (keys) and their corresponding sort directions (values). This can |
||
75 | * be passed to [[\yii\db\Query::orderBy()]] to construct a DB query. This property is read-only. |
||
76 | * |
||
77 | * @author Qiang Xue <[email protected]> |
||
78 | * @since 2.0 |
||
79 | */ |
||
80 | class Sort extends BaseObject |
||
81 | { |
||
82 | /** |
||
83 | * @var bool whether the sorting can be applied to multiple attributes simultaneously. |
||
84 | * Defaults to `false`, which means each time the data can only be sorted by one attribute. |
||
85 | */ |
||
86 | public $enableMultiSort = false; |
||
87 | /** |
||
88 | * @var array list of attributes that are allowed to be sorted. Its syntax can be |
||
89 | * described using the following example: |
||
90 | * |
||
91 | * ```php |
||
92 | * [ |
||
93 | * 'age', |
||
94 | * 'name' => [ |
||
95 | * 'asc' => ['first_name' => SORT_ASC, 'last_name' => SORT_ASC], |
||
96 | * 'desc' => ['first_name' => SORT_DESC, 'last_name' => SORT_DESC], |
||
97 | * 'default' => SORT_DESC, |
||
98 | * 'label' => 'Name', |
||
99 | * ], |
||
100 | * ] |
||
101 | * ``` |
||
102 | * |
||
103 | * In the above, two attributes are declared: `age` and `name`. The `age` attribute is |
||
104 | * a simple attribute which is equivalent to the following: |
||
105 | * |
||
106 | * ```php |
||
107 | * 'age' => [ |
||
108 | * 'asc' => ['age' => SORT_ASC], |
||
109 | * 'desc' => ['age' => SORT_DESC], |
||
110 | * 'default' => SORT_ASC, |
||
111 | * 'label' => Inflector::camel2words('age'), |
||
112 | * ] |
||
113 | * ``` |
||
114 | * |
||
115 | * Since 2.0.12 particular sort direction can be also specified as direct sort expression, like following: |
||
116 | * |
||
117 | * ```php |
||
118 | * 'name' => [ |
||
119 | * 'asc' => '[[last_name]] ASC NULLS FIRST', // PostgreSQL specific feature |
||
120 | * 'desc' => '[[last_name]] DESC NULLS LAST', |
||
121 | * ] |
||
122 | * ``` |
||
123 | * |
||
124 | * The `name` attribute is a composite attribute: |
||
125 | * |
||
126 | * - The `name` key represents the attribute name which will appear in the URLs leading |
||
127 | * to sort actions. |
||
128 | * - The `asc` and `desc` elements specify how to sort by the attribute in ascending |
||
129 | * and descending orders, respectively. Their values represent the actual columns and |
||
130 | * the directions by which the data should be sorted by. |
||
131 | * - The `default` element specifies by which direction the attribute should be sorted |
||
132 | * if it is not currently sorted (the default value is ascending order). |
||
133 | * - The `label` element specifies what label should be used when calling [[link()]] to create |
||
134 | * a sort link. If not set, [[Inflector::camel2words()]] will be called to get a label. |
||
135 | * Note that it will not be HTML-encoded. |
||
136 | * |
||
137 | * Note that if the Sort object is already created, you can only use the full format |
||
138 | * to configure every attribute. Each attribute must include these elements: `asc` and `desc`. |
||
139 | */ |
||
140 | public $attributes = []; |
||
141 | /** |
||
142 | * @var string the name of the parameter that specifies which attributes to be sorted |
||
143 | * in which direction. Defaults to `sort`. |
||
144 | * @see params |
||
145 | */ |
||
146 | public $sortParam = 'sort'; |
||
147 | /** |
||
148 | * @var array the order that should be used when the current request does not specify any order. |
||
149 | * The array keys are attribute names and the array values are the corresponding sort directions. For example, |
||
150 | * |
||
151 | * ```php |
||
152 | * [ |
||
153 | * 'name' => SORT_ASC, |
||
154 | * 'created_at' => SORT_DESC, |
||
155 | * ] |
||
156 | * ``` |
||
157 | * |
||
158 | * @see attributeOrders |
||
159 | */ |
||
160 | public $defaultOrder; |
||
161 | /** |
||
162 | * @var string the route of the controller action for displaying the sorted contents. |
||
163 | * If not set, it means using the currently requested route. |
||
164 | */ |
||
165 | public $route; |
||
166 | /** |
||
167 | * @var string the character used to separate different attributes that need to be sorted by. |
||
168 | */ |
||
169 | public $separator = ','; |
||
170 | /** |
||
171 | * @var array parameters (name => value) that should be used to obtain the current sort directions |
||
172 | * and to create new sort URLs. If not set, `$_GET` will be used instead. |
||
173 | * |
||
174 | * In order to add hash to all links use `array_merge($_GET, ['#' => 'my-hash'])`. |
||
175 | * |
||
176 | * The array element indexed by [[sortParam]] is considered to be the current sort directions. |
||
177 | * If the element does not exist, the [[defaultOrder|default order]] will be used. |
||
178 | * |
||
179 | * @see sortParam |
||
180 | * @see defaultOrder |
||
181 | */ |
||
182 | public $params; |
||
183 | /** |
||
184 | * @var \yii\web\UrlManager the URL manager used for creating sort URLs. If not set, |
||
185 | * the `urlManager` application component will be used. |
||
186 | */ |
||
187 | public $urlManager; |
||
188 | /** |
||
189 | * @var int Allow to control a value of the fourth parameter which will be |
||
190 | * passed to [[ArrayHelper::multisort()]] |
||
191 | * @since 2.0.33 |
||
192 | */ |
||
193 | public $sortFlags = SORT_REGULAR; |
||
194 | |||
195 | |||
196 | /** |
||
197 | * Normalizes the [[attributes]] property. |
||
198 | */ |
||
199 | 95 | public function init() |
|
200 | { |
||
201 | 95 | $attributes = []; |
|
202 | 95 | foreach ($this->attributes as $name => $attribute) { |
|
203 | 25 | if (!is_array($attribute)) { |
|
204 | 20 | $attributes[$attribute] = [ |
|
205 | 20 | 'asc' => [$attribute => SORT_ASC], |
|
206 | 20 | 'desc' => [$attribute => SORT_DESC], |
|
207 | ]; |
||
208 | 12 | } elseif (!isset($attribute['asc'], $attribute['desc'])) { |
|
209 | $attributes[$name] = array_merge([ |
||
210 | 'asc' => [$name => SORT_ASC], |
||
211 | 'desc' => [$name => SORT_DESC], |
||
212 | ], $attribute); |
||
213 | } else { |
||
214 | 25 | $attributes[$name] = $attribute; |
|
215 | } |
||
216 | } |
||
217 | 95 | $this->attributes = $attributes; |
|
218 | 95 | } |
|
219 | |||
220 | /** |
||
221 | * Returns the columns and their corresponding sort directions. |
||
222 | * @param bool $recalculate whether to recalculate the sort directions |
||
223 | * @return array the columns (keys) and their corresponding sort directions (values). |
||
224 | * This can be passed to [[\yii\db\Query::orderBy()]] to construct a DB query. |
||
225 | */ |
||
226 | 85 | public function getOrders($recalculate = false) |
|
227 | { |
||
228 | 85 | $attributeOrders = $this->getAttributeOrders($recalculate); |
|
229 | 85 | $orders = []; |
|
230 | 85 | foreach ($attributeOrders as $attribute => $direction) { |
|
231 | 15 | $definition = $this->attributes[$attribute]; |
|
232 | 15 | $columns = $definition[$direction === SORT_ASC ? 'asc' : 'desc']; |
|
233 | 15 | if (is_array($columns) || $columns instanceof \Traversable) { |
|
234 | 14 | foreach ($columns as $name => $dir) { |
|
235 | 14 | $orders[$name] = $dir; |
|
236 | } |
||
237 | } else { |
||
238 | 15 | $orders[] = $columns; |
|
239 | } |
||
240 | } |
||
241 | |||
242 | 85 | return $orders; |
|
243 | } |
||
244 | |||
245 | /** |
||
246 | * @var array the currently requested sort order as computed by [[getAttributeOrders]]. |
||
247 | */ |
||
248 | private $_attributeOrders; |
||
249 | |||
250 | /** |
||
251 | * Returns the currently requested sort information. |
||
252 | * @param bool $recalculate whether to recalculate the sort directions |
||
253 | * @return array sort directions indexed by attribute names. |
||
254 | * Sort direction can be either `SORT_ASC` for ascending order or |
||
255 | * `SORT_DESC` for descending order. |
||
256 | */ |
||
257 | 94 | public function getAttributeOrders($recalculate = false) |
|
258 | { |
||
259 | 94 | if ($this->_attributeOrders === null || $recalculate) { |
|
260 | 93 | $this->_attributeOrders = []; |
|
261 | 93 | if (($params = $this->params) === null) { |
|
262 | 76 | $request = Yii::$app->getRequest(); |
|
263 | 76 | $params = $request instanceof Request ? $request->getQueryParams() : []; |
|
264 | } |
||
265 | 93 | if (isset($params[$this->sortParam])) { |
|
266 | 16 | foreach ($this->parseSortParam($params[$this->sortParam]) as $attribute) { |
|
267 | 16 | $descending = false; |
|
268 | 16 | if (strncmp($attribute, '-', 1) === 0) { |
|
269 | 16 | $descending = true; |
|
270 | 16 | $attribute = substr($attribute, 1); |
|
271 | } |
||
272 | |||
273 | 16 | if (isset($this->attributes[$attribute])) { |
|
274 | 16 | $this->_attributeOrders[$attribute] = $descending ? SORT_DESC : SORT_ASC; |
|
275 | 16 | if (!$this->enableMultiSort) { |
|
276 | 16 | return $this->_attributeOrders; |
|
277 | } |
||
278 | } |
||
279 | } |
||
280 | } |
||
281 | 84 | if (empty($this->_attributeOrders) && is_array($this->defaultOrder)) { |
|
282 | 5 | $this->_attributeOrders = $this->defaultOrder; |
|
283 | } |
||
284 | } |
||
285 | |||
286 | 85 | return $this->_attributeOrders; |
|
287 | } |
||
288 | |||
289 | /** |
||
290 | * Parses the value of [[sortParam]] into an array of sort attributes. |
||
291 | * |
||
292 | * The format must be the attribute name only for ascending |
||
293 | * or the attribute name prefixed with `-` for descending. |
||
294 | * |
||
295 | * For example the following return value will result in ascending sort by |
||
296 | * `category` and descending sort by `created_at`: |
||
297 | * |
||
298 | * ```php |
||
299 | * [ |
||
300 | * 'category', |
||
301 | * '-created_at' |
||
302 | * ] |
||
303 | * ``` |
||
304 | * |
||
305 | * @param string $param the value of the [[sortParam]]. |
||
306 | * @return array the valid sort attributes. |
||
307 | * @since 2.0.12 |
||
308 | * @see $separator for the attribute name separator. |
||
309 | * @see $sortParam |
||
310 | */ |
||
311 | 15 | protected function parseSortParam($param) |
|
312 | { |
||
313 | 15 | return is_scalar($param) ? explode($this->separator, $param) : []; |
|
0 ignored issues
–
show
introduced
by
Loading history...
|
|||
314 | } |
||
315 | |||
316 | /** |
||
317 | * Sets up the currently sort information. |
||
318 | * @param array|null $attributeOrders sort directions indexed by attribute names. |
||
319 | * Sort direction can be either `SORT_ASC` for ascending order or |
||
320 | * `SORT_DESC` for descending order. |
||
321 | * @param bool $validate whether to validate given attribute orders against [[attributes]] and [[enableMultiSort]]. |
||
322 | * If validation is enabled incorrect entries will be removed. |
||
323 | * @since 2.0.10 |
||
324 | */ |
||
325 | 1 | public function setAttributeOrders($attributeOrders, $validate = true) |
|
326 | { |
||
327 | 1 | if ($attributeOrders === null || !$validate) { |
|
328 | 1 | $this->_attributeOrders = $attributeOrders; |
|
329 | } else { |
||
330 | 1 | $this->_attributeOrders = []; |
|
331 | 1 | foreach ($attributeOrders as $attribute => $order) { |
|
332 | 1 | if (isset($this->attributes[$attribute])) { |
|
333 | 1 | $this->_attributeOrders[$attribute] = $order; |
|
334 | 1 | if (!$this->enableMultiSort) { |
|
335 | 1 | break; |
|
336 | } |
||
337 | } |
||
338 | } |
||
339 | } |
||
340 | 1 | } |
|
341 | |||
342 | /** |
||
343 | * Returns the sort direction of the specified attribute in the current request. |
||
344 | * @param string $attribute the attribute name |
||
345 | * @return int|null Sort direction of the attribute. Can be either `SORT_ASC` |
||
346 | * for ascending order or `SORT_DESC` for descending order. Null is returned |
||
347 | * if the attribute is invalid or does not need to be sorted. |
||
348 | */ |
||
349 | 6 | public function getAttributeOrder($attribute) |
|
350 | { |
||
351 | 6 | $orders = $this->getAttributeOrders(); |
|
352 | |||
353 | 6 | return isset($orders[$attribute]) ? $orders[$attribute] : null; |
|
354 | } |
||
355 | |||
356 | /** |
||
357 | * Generates a hyperlink that links to the sort action to sort by the specified attribute. |
||
358 | * Based on the sort direction, the CSS class of the generated hyperlink will be appended |
||
359 | * with "asc" or "desc". |
||
360 | * @param string $attribute the attribute name by which the data should be sorted by. |
||
361 | * @param array $options additional HTML attributes for the hyperlink tag. |
||
362 | * There is one special attribute `label` which will be used as the label of the hyperlink. |
||
363 | * If this is not set, the label defined in [[attributes]] will be used. |
||
364 | * If no label is defined, [[\yii\helpers\Inflector::camel2words()]] will be called to get a label. |
||
365 | * Note that it will not be HTML-encoded. |
||
366 | * @return string the generated hyperlink |
||
367 | * @throws InvalidConfigException if the attribute is unknown |
||
368 | */ |
||
369 | 4 | public function link($attribute, $options = []) |
|
370 | { |
||
371 | 4 | if (($direction = $this->getAttributeOrder($attribute)) !== null) { |
|
372 | 1 | $class = $direction === SORT_DESC ? 'desc' : 'asc'; |
|
373 | 1 | if (isset($options['class'])) { |
|
374 | $options['class'] .= ' ' . $class; |
||
375 | } else { |
||
376 | 1 | $options['class'] = $class; |
|
377 | } |
||
378 | } |
||
379 | |||
380 | 4 | $url = $this->createUrl($attribute); |
|
381 | 4 | $options['data-sort'] = $this->createSortParam($attribute); |
|
382 | |||
383 | 4 | if (isset($options['label'])) { |
|
384 | $label = $options['label']; |
||
385 | unset($options['label']); |
||
386 | } else { |
||
387 | 4 | if (isset($this->attributes[$attribute]['label'])) { |
|
388 | 2 | $label = $this->attributes[$attribute]['label']; |
|
389 | } else { |
||
390 | 2 | $label = Inflector::camel2words($attribute); |
|
391 | } |
||
392 | } |
||
393 | |||
394 | 4 | return Html::a($label, $url, $options); |
|
395 | } |
||
396 | |||
397 | /** |
||
398 | * Creates a URL for sorting the data by the specified attribute. |
||
399 | * This method will consider the current sorting status given by [[attributeOrders]]. |
||
400 | * For example, if the current page already sorts the data by the specified attribute in ascending order, |
||
401 | * then the URL created will lead to a page that sorts the data by the specified attribute in descending order. |
||
402 | * @param string $attribute the attribute name |
||
403 | * @param bool $absolute whether to create an absolute URL. Defaults to `false`. |
||
404 | * @return string the URL for sorting. False if the attribute is invalid. |
||
405 | * @throws InvalidConfigException if the attribute is unknown |
||
406 | * @see attributeOrders |
||
407 | * @see params |
||
408 | */ |
||
409 | 5 | public function createUrl($attribute, $absolute = false) |
|
410 | { |
||
411 | 5 | if (($params = $this->params) === null) { |
|
412 | 3 | $request = Yii::$app->getRequest(); |
|
413 | 3 | $params = $request instanceof Request ? $request->getQueryParams() : []; |
|
414 | } |
||
415 | 5 | $params[$this->sortParam] = $this->createSortParam($attribute); |
|
416 | 5 | $params[0] = $this->route === null ? Yii::$app->controller->getRoute() : $this->route; |
|
417 | 5 | $urlManager = $this->urlManager === null ? Yii::$app->getUrlManager() : $this->urlManager; |
|
418 | 5 | if ($absolute) { |
|
419 | return $urlManager->createAbsoluteUrl($params); |
||
420 | } |
||
421 | |||
422 | 5 | return $urlManager->createUrl($params); |
|
423 | } |
||
424 | |||
425 | /** |
||
426 | * Creates the sort variable for the specified attribute. |
||
427 | * The newly created sort variable can be used to create a URL that will lead to |
||
428 | * sorting by the specified attribute. |
||
429 | * @param string $attribute the attribute name |
||
430 | * @return string the value of the sort variable |
||
431 | * @throws InvalidConfigException if the specified attribute is not defined in [[attributes]] |
||
432 | */ |
||
433 | 6 | public function createSortParam($attribute) |
|
434 | { |
||
435 | 6 | if (!isset($this->attributes[$attribute])) { |
|
436 | throw new InvalidConfigException("Unknown attribute: $attribute"); |
||
437 | } |
||
438 | 6 | $definition = $this->attributes[$attribute]; |
|
439 | 6 | $directions = $this->getAttributeOrders(); |
|
440 | 6 | if (isset($directions[$attribute])) { |
|
441 | 3 | $direction = $directions[$attribute] === SORT_DESC ? SORT_ASC : SORT_DESC; |
|
442 | 3 | unset($directions[$attribute]); |
|
443 | } else { |
||
444 | 3 | $direction = isset($definition['default']) ? $definition['default'] : SORT_ASC; |
|
445 | } |
||
446 | |||
447 | 6 | if ($this->enableMultiSort) { |
|
448 | 3 | $directions = array_merge([$attribute => $direction], $directions); |
|
449 | } else { |
||
450 | 3 | $directions = [$attribute => $direction]; |
|
451 | } |
||
452 | |||
453 | 6 | $sorts = []; |
|
454 | 6 | foreach ($directions as $attribute => $direction) { |
|
455 | 6 | $sorts[] = $direction === SORT_DESC ? '-' . $attribute : $attribute; |
|
456 | } |
||
457 | |||
458 | 6 | return implode($this->separator, $sorts); |
|
459 | } |
||
460 | |||
461 | /** |
||
462 | * Returns a value indicating whether the sort definition supports sorting by the named attribute. |
||
463 | * @param string $name the attribute name |
||
464 | * @return bool whether the sort definition supports sorting by the named attribute. |
||
465 | */ |
||
466 | public function hasAttribute($name) |
||
467 | { |
||
468 | return isset($this->attributes[$name]); |
||
469 | } |
||
470 | } |
||
471 |